USB Keyboard Not Working in Dracut When Connected via Thunderbolt Dock

My USB keyboard stopped working during the Dracut initramfs phase (e.g., at the LUKS password prompt) when connected through a Thunderbolt dock. It worked fine in GRUB, in GNOME, and when plugged directly into the laptop. It had also worked through the dock before.

Why It Broke

GRUB is probably using UEFI/BIOS USB legacy emulation and doesn’t need the Thunderbolt controller at all. Dracut uses the real kernel driver stack, so the Thunderbolt controller needs to be initialized and authorized before the keyboard becomes visible.

Checking the Thunderbolt security level:

$ cat /sys/bus/thunderbolt/devices/domain0/security
user

The user level requires explicit device authorization. In GNOME, boltd handles this automatically. In Dracut, nothing does. Previously the security level was none, but a firmware update changed it to user.

IOMMU DMA protection is still active independently:

$ cat /sys/bus/thunderbolt/devices/domain0/iommu_dma_protection
1

Why Not Boot ACL?

The ideal fix would be to enroll the dock in the firmware’s Boot ACL — pre-authorized devices stored in UEFI NVRAM that are authorized before the OS loads. However, boltctl domains showed bootacl: 0/0 — the firmware doesn’t support it.

The Fix: A Dracut-Only Udev Rule

The solution is a udev rule that auto-authorizes Thunderbolt devices during the initramfs phase only. We don’t want this rule in the running system, as it would bypass boltd’s authorization logic in GNOME. The clean way is a small dracut module that carries the udev rule inside the initramfs.

Create the module directory and files:

$ sudo mkdir -p /usr/lib/dracut/modules.d/99thunderbolt-auth

$ sudo tee /usr/lib/dracut/modules.d/99thunderbolt-auth/99-thunderbolt-auto-auth.rules <<'EOF'
ACTION=="add", SUBSYSTEM=="thunderbolt", ATTR{authorized}=="0", ATTR{authorized}="1"
EOF

$ sudo tee /usr/lib/dracut/modules.d/99thunderbolt-auth/module-setup.sh <<'EOF'
#!/bin/bash
check() { return 0; }
depends() { return 0; }
install() {
    inst_simple "$moddir/99-thunderbolt-auto-auth.rules" \
        /etc/udev/rules.d/99-thunderbolt-auto-auth.rules
}
EOF

$ sudo chmod +x /usr/lib/dracut/modules.d/99thunderbolt-auth/module-setup.sh

Create the dracut config at /etc/dracut.conf.d/thunderbolt.conf:

add_dracutmodules+=" thunderbolt-auth "

Note that dracut module names in config files omit the numeric prefix — the directory is 99thunderbolt-auth but is referenced as thunderbolt-auth.

Rebuild the initramfs:

$ sudo dracut --force

Security Notes

The udev rule auto-authorizes Thunderbolt devices only during the brief Dracut window. In the running system, boltd continues to handle authorization normally. In both cases, IOMMU DMA protection remains active, which is the actual security boundary against malicious Thunderbolt devices.

Auto-switch power-profiles-daemon with udev

On GNOME with power-profiles-daemon, I wanted power profiles to flip between power-saver and performance based on AC/battery. The solution is to let udev react to POWER_SUPPLY_ONLINE changes and call powerprofilesctl directly.

Configuration

Put the following rules in /etc/udev/rules.d/99-power-profile-switch.rules:

$ sudo tee /etc/udev/rules.d/99-power-profile-switch.rules >/dev/null <<'EOF'
# AC plugged in
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ENV{POWER_SUPPLY_ONLINE}=="1", ACTION=="change", RUN+="/usr/bin/powerprofilesctl set performance"

# On battery
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ENV{POWER_SUPPLY_ONLINE}=="0", ACTION=="change", RUN+="/usr/bin/powerprofilesctl set power-saver"
EOF

Reload udev and trigger a change:

$ sudo udevadm control --reload-rules
$ sudo udevadm trigger --subsystem-match=power_supply

Verify

Unplug and replug the AC adapter, then check the active profile:

$ powerprofilesctl get

Stop Slack from Spamming systemd Journal Logs

Slack has been spamming my systemd journal logs with useless info level debug logs. I’ve tried starting slack with --silent and setting the log level to fatal using --logLevel, but the log spamming hasn’t stopped.

The solution is to filter these messages using rsyslog.

Create the following configuration under /etc/rsyslog.d/20-slack.conf:

# Drop info log messages from Slack
if $rawmsg contains "slack.desktop" then stop

Verify the configuration, then restart rsyslog:

$ sudo rsyslogd -N1
$ sudo systemctl restart rsyslog

Update: Slack used a different .desktop file for autostart, which didn’t have my -s -g fatal switches. I believe editing the ~/.config/autostart/slack.desktop file should be enough to fix the issue.

Configuring LDAC Quality in PipeWire

You can set the LDAC quality between High, Standard, and Mobile quality, corresponding to 990/660/330 kbps. You can do it either statically or dynamically.

Static Configuration

Place the following configuration in ~/.config/wireplumber/wireplumber.conf.d/10-bluez.conf:

monitor.bluez.rules = [
  {
    matches = [
      {
        ## This matches all Bluetooth devices.
        device.name = "~bluez_card.*"
      }
    ]
    actions = {
      update-props = {
        bluez5.a2dp.ldac.quality = "sq"
      }
    }
  }
]

The value of quality can be set to either hq, sq, mq, or auto.

You can also change the match fragment to match only a specific device. Use the following command to list all currently available devices:

$ pw-cli ls | grep device.name

After changing the configuration, you’ll have to restart WirePlumber:

$ systemctl --user restart wireplumber

Dynamic Configuration

This method is less user-friendly. The first step is to find the id of the relevant output node. You can do this by examining the output of pw-cli ls or wpctl status. Make sure you pick the id of the corresponding node and not the device. Next, use pw-cli set-param to set the quality, for example:

$ pw-cli set-param 93 Props '{quality=0}'
Object: size 32, type Spa:Pod:Object:Param:Props (262146), id Spa:Enum:ParamId:Props (2)
  Prop: key Spa:Pod:Object:Param:Props:quality (269), flags 00000000
    Int 0

Where 93 is our node id, and 0 corresponds to hq quality. Other possible values are: -1 for auto, 0 for hq, 1 for sq, and 2 for mq.

Empirically Verifying Bitrate

You can deduce the actual bitrate by sniffing the Bluetooth traffic and analyzing the capture using Wireshark.

$ sudo btmon -w btsnoop.log
$ wireshark btsnoop.log

In Wireshark, go to Statistics -> Capture File Properties, and there you can see the average bits/s and compare it to the bitrate of the expected quality setting.

Setting Up a Swap File on BTRFS

With the release of btrfs-progs 6.1 (available in Debian Bookworm), you can create a swap file using the btrfs utility. It will take care of both preallocating the file and marking it as NODATACOW. As BTRFS can’t snapshot a subvolume that contains an active swap file, we will create a new subvolume for the swap file to reside in.

$ sudo btrfs subvolume create /swap
$ sudo btrfs filesystem mkswapfile --size 4g /swap/swapfile
$ sudo swapon /swap/swapfile

To auto-activate the swap file at boot, add the following line to /etc/fstab:

/swap/swapfile none swap defaults 0 0

Encrypted swap file

My entire root filesystem is encrypted, but having unencrypted swap can still lead to sensitive data being inadvertently exposed. The solution is to encrypt the swap file using random keys at boot. Add the following line to /etc/crypttab:

swap /swap/swapfile  /dev/urandom swap,cipher=aes-xts-plain64

And change the swap file location in /etc/fstab to point to /dev/mapper/swap like this:

/dev/mapper/swap none swap defaults 0 0

The new swap file will automatically start upon boot. To start it immediately, run:

$ sudo systemctl daemon-reload
$ sudo systemctl start systemd-cryptsetup@swap.service
$ sudo swapon /dev/mapper/swap

PSI-Notify

Adding a small amount of swap can significantly help in preventing the system from running out of memory. psi-notify can alert you when your running low on memory.

$ sudo apt install psi-notify
$ systemctl --user start psi-notify

Add the following line to ~/.config/psi-notify:

threshold memory some avg10 2.00

You may need to adjust the threshold to ensure it triggers at the right time for your needs. To test memory pressure, you can use the following command:

$ </dev/zero head -c 20G | tail

Fixing the Out of Memory Error When Installing Interactive Brokers TWS on Linux

When installing Interactive Brokers TWS on Linux, I encountered the following error after the installer unpacked the Java Runtime Environment (JRE):

library initialization failed - unable to allocate file descriptor table - out of memoryAborted

The solution was to increase the open file limit before running the installer:

$ ulimit -n 10000
$ ./tws-stable-linux-x64.sh

Convert PKCS#7 Certificate Chain to PEM

I’m trying to use certificates issued by Microsoft Active Directory Certificate Services (AD CS) to connect to an 802.1x protected network. NetworkManager expects certificates in PEM format, but AD CS issues them in PKCS#7 format (with a .p7b extension). You can use OpenSSL to convert the certificates:

openssl pkcs7 -print_certs -inform DER -in certnew.p7b -out cert-chain.pem

In this command, certnew.p7b is the PKCS#7 encoded certificate chain you received from AD CS, and cert-chain.pem is the desired output file.

Running Concept2 Utility in a VM

II recently found myself needing to transfer some old workout data from my Concept2 PM3 and its associated Logcard. However, as I didn’t have easy access to a Windows machine, and Concept2 only provides their utility software for Windows and Mac, I was compelled to run it in a Windows VM.

Upon connecting the PM3 to my computer and redirecting the USB device to the VM, I encountered an issue: the Concept2 Utility failed to recognize the connected PM3. In an attempt to resolve this, I downgraded the Concept2 Utility to an older version, 6.54. This version did recognize the PM3, but it still failed to recognize the Logcard.

The solution I found was to add the PM3 as a USB host device, rather than using USB redirection. This can be accomplished via the VM’s hardware detail page by selecting Add Hardware -> USB Host Device, or by using the following XML configuration:

<hostdev mode="subsystem" type="usb" managed="yes">
  <source>
    <vendor id="0x17a4"/>
    <product id="0x0001"/>
  </source>
  <address type="usb" bus="0" port="3"/>
</hostdev>

The Vendor ID/Product ID (VID/PID) shown above corresponds to the PM3’s VID/PID. If you’re using a different monitor, you may need to adjust these values accordingly.

For reference, here is an example of how the PM3’s VID/PID appears:

Bus 003 Device 004: ID 17a4:0001 Concept2 Performance Monitor 3

Additional details about the setup include:

  • Windows 10 VM
  • QEMU/KVM virtualization via virt-manager.
  • Concept2 Utility verison 7.14

Regenerate Dracut initramfs images from a live USB

Thanks to an incompatability between dracut and systemd version 256, I was left with an unbootable system. I booted a live USB system, downgraded systemd back to 255, but I had to regenerate the initramfs images to be compatible with my old systemd.

While using systemd-nspawn is convenient for modifying the system from a live usb, generating dracut initramfs images through it, resulted in missing hardware support (for example, no nvme module) due to the abstracted hardware in the container. The solution is to revert to the traditional chroot and generate the images from there:

$ sudo /usr/lib/systemd/systemd-cryptsetup attach root /dev/nvme0n1p3
$ sudo mount /dev/mapper/root /mnt/
$ sudo mount /dev/nvme0n1p2 /mnt/boot/
$ sudo mount --bind /dev/ /mnt/dev/
$ sudo mount --bind /proc/ /mnt/proc/
$ sudo mount --bind /sys/ /mnt/sys/
$ sudo chroot /mnt/
# dracut -f --regenerate-all

Automating DNS Configurations for F5 VPN Tunnel using Systemd-resolved and NetworkManager-dispatcher

F5 VPN does not play well with split DNS configuration using systemd-resolved because it insists on trying to rewrite /etc/resolv.conf. The workaround is to make resolv.conf immutable, and configure the DNS settings for the tunnel manually. systemd-resolved does not have a mechanism for persistant per-interface configuration, and it relies on NetworkManager to configure each connection correctly. F5 VPN is not compatible with NetworkManager, and does not make it easy to configure it this way.

NetworkManager-dispatcher allows you to run scripts based on network events. In our case, we will use it to automatically add DNS configurations when the F5 VPN tunnel tun0 is up, and thus provide persistent configuration.

Here is the script:

#!/bin/bash

INTERFACE=$1
STATUS=$2

case "$STATUS" in
    'up')
        if [ "$INTERFACE" = "tun0" ]; then
            # Add your search domains here
            SEARCH_DOMAINS="~example.corp ~example.local"

            resolvectl domain "$INTERFACE" $SEARCH_DOMAINS
            resolvectl dns $INTERFACE 192.168.100.20 192.168.100.22
            resolvectl dnsovertls tun0 no
        fi
        ;;
esac

The script checks if the interface is tun0 and if the current action is up. If so, it uses resolvectl to configure search domains and local DNS servers. Lastly, DNS over TLS is disabled, as the corporate DNS servers do not support them.

To make this script work, install in the /etc/NetworkManager/dispatcher.d/ directory with the name f5-vpn. Make sure it’s executable and only writable by root. NetworkManager-dispatcher will run this script whenever a network interface goes up, automatically setting the DNS configurations for F5 VPN tunnel.