I recently starting using Yubikeys both to store passkeys which allow me to do passwordless logins to websites like GitHub, and to SSH into remote servers with FIDO2.

I have a number of machines at home, but I spend the majority of my time using a Windows 11 desktop computer running NixOS on WSL2 (in the past I’ve described Windows 11 + my tiling window manager komorebi as the “desktop environment” on top of my NixOS WSL2 shell).

Rarely if ever do I make SSH connections from Windows 11 directly; if I am making an SSH connection it is almost always from NixOS. This posed a problem for my adoption of FIDO2 SSH with my Yubikeys, because the process is not quite as simple as just passing through the USB Yubikey to the WSL2 VM.

Below I’ll outline the steps to get USB Yubikey passthrough to a NixOS WSL2 VM working with full FIDO2 support.

Although these steps specifically target NixOS, the underlying information can be used to produce the same result on the Linux distribution of your choice.

Prerequisites on Windows 11

Get started by installing Yubikey Manager on Windows and making sure your Yubikey(s) are being recognized:

winget install -e --id Yubico.YubikeyManager

# Unfortunately, WinGet _still_ isn't able to place installed binaries in the $PATH reliably 🤦

 & "C:\Program Files\Yubico\YubiKey Manager\ykman.exe" info
Device type: YubiKey 5C
Serial number: XXXXXXXXXX
Firmware version: 5.4.3
Form factor: Keychain (USB-C)
Enabled USB interfaces: OTP, FIDO, CCID

Applications
OTP             Enabled
FIDO U2F        Enabled
FIDO2           Enabled
OATH            Enabled
PIV             Enabled
OpenPGP         Enabled
YubiHSM Auth    Enabled

Next, install usbipd-win, which is what we’ll use to do the USB passthrough to the WSL2 VM.

# For whatever reason, this one does seem to be added to the $PATH correctly 🤷

winget install usbipd

In an Administrator PowerShell Terminal, run usbipd list and take note of the BUSID for your Yubikey, which we will need later:

 usbipd list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
9-4    1050:0407  USB Input Device, Microsoft Usbccid Smartcard Reader (WUDF)   Not shared

Building and Loading a Custom WSL2 Linux Kernel

Now comes the fun part.

In order to enable full USB passthrough with FIDO2 support, we need to compile a custom WSL2 Linux kernel, because the kernels released by Microsoft are for whatever reason missing a few key options that we need.

If you’ve already compiled your own WSL2 Linux Kernels before and are comfortable with this process, you just need to go and enable HIDDEV and HIDRAW and then recompile.

Otherwise, you can navigate to my custom-wsl2-linux-kernel project and download the latest release. This project pulls the latest version of the offical WSL2-Linux-Kernel released by Microsoft, enables the required configuration options, builds the kernel on GitHub Actions, and finally makes the resulting vmlinux file available to download.

If you are interested in building WSL2 Linux kernels with different options enabled, you can fork the project and commit your own edits to the config-wsl file. GitHub Actions will then start building the kernel for you and store the resulting vmlinux file as a build artifact for you to download.

Anyway, once you have your vmlinux file, place it in a convenient directory (I keep mine in $HOME 🤷) and then edit (or create) your ~/.wslconfig file on Windows 11 to instruct WSL2 to use this specific kernel when booting VMs:

[wsl2]
kernel=C:\\Users\\LGUG2Z\\vmlinux

Now make sure you’ve saved any work you’re doing in any WSL2 VMs and run wsl --shutdown in a PowerShell prompt. Then wait for at least 10 seconds after that command terminates before starting up your NixOS WSL2 VM again.

Configuring NixOS to Automatically Attach Your Yubikey

Thankfully, now that we are back in NixOS land the rest of this tutorial is fairly declarative.

I must give a huge thank you to everyone who has been contributing to the various threads about this on the NixOS-WSL repo, in particular the user terlar who has put together a pending PR which the Nix code below is largely based on.

Start by creating a usbip.nix file and storing it wherever feels best in your flake repo:

{
  config,
  lib,
  pkgs,
  ...
}:
with lib; let
  usbipd-win-auto-attach = pkgs.fetchurl {
    url = "https://raw.githubusercontent.com/dorssel/usbipd-win/v3.1.0/Usbipd/wsl-scripts/auto-attach.sh";
    hash = "sha256-KJ0tEuY+hDJbBQtJj8nSNk17FHqdpDWTpy9/DLqUFaM=";
  };

  cfg = config.wsl.usbip;
in {
  options.wsl.usbip = with types; {
    enable = mkEnableOption "USB/IP integration";
    autoAttach = mkOption {
      type = listOf str;
      default = [];
      example = ["4-1"];
      description = "Auto attach devices with provided Bus IDs.";
    };
  };

  config = mkIf (config.wsl.enable && cfg.enable) {
    environment.systemPackages = [
      pkgs.linuxPackages.usbip
      pkgs.yubikey-manager
      pkgs.libfido2
    ];

    services.pcscd.enable = true;
    services.udev = {
      enable = true;
      packages = [pkgs.yubikey-personalization];
      extraRules = ''
        SUBSYSTEM=="usb", MODE="0666"
        KERNEL=="hidraw*", SUBSYSTEM=="hidraw", TAG+="uaccess", MODE="0666"
      '';
    };

    systemd = {
      services."usbip-auto-attach@" = {
        description = "Auto attach device having busid %i with usbip";
        after = ["network.target"];

        scriptArgs = "%i";
        path = [pkgs.linuxPackages.usbip];

        script = ''
          busid="$1"
          ip="$(grep nameserver /etc/resolv.conf | cut -d' ' -f2)"

          echo "Starting auto attach for busid $busid on $ip."
          source ${usbipd-win-auto-attach} "$ip" "$busid"
        '';
      };

      targets.multi-user.wants = map (busid: "usbip-auto-attach@${busid}.service") cfg.autoAttach;
    };
  };
}

Then, in the part of your flake that deals with your WSL configuration, import the file and set the appropriate BUSID for your Yubikey which we determined earlier:

{
  imports = [
    # Your import path will probably be different
    ./usbip.nix
  ];

  wsl = {
    usbip = {
      enable = true;
      # Replace this with the BUSID for your Yubikey
      autoAttach = ["9-4"];
    };
  };
}

Go ahead and rebuild your system with your updated NixOS configuration, and now whenever your Yubikey is detected in the port that you have identified, it will be passed through automatically to your NixOS VM. 🎉

We can test this by running ykman fido credentials list inside of our WSL2 VM:

❯ ykman fido credentials list
Enter your PIN: ************
Credential ID  RP ID              Username               Display name
abcdefgh...    github.com         LGUG2Z                 جاد

Great! You can now use your existing FIDO2 SSH keys from inside your WSL2 VM, or generate a new key by following the Linux setup instructions for “Securing SSH with FIDO2” on the Yubico website.

A few things to keep in mind:

  • You’re probably going to want to keep two Yubikeys connected to your Windows machine simultaneously in order to use one in Windows and one in WSL2
  • If you have both Yubikeys connected simultaneously, and you have set up Yubico Login for Windows, you’ll have to remove one of them when you are logging into the computer after a restart otherwise the login will fail
  • Outside of this specific edge case, I haven’t encountered any other issues with keeping two Yubikeys connected simultaneously for daily use
  • In order to use FIDO2 functionality, you need to explicitly set up a FIDO2 PIN on your Yubikey as one is not set by default

I’m Not Sure I Can Replicate This on Ubuntu…

The main things to note if you are trying to adapt the NixOS configuration snippets above to work on other Linux distrubtions are:

  • The packages being added to the system environment
  • The auto-attach.sh script being pulled from the usbipd-win project
  • The udev rules being set for usb and hidraw*
  • The systemd service which calls the auto-attach.sh script and attaches the device on the given BUSID to the VM

Look, I’m not gonna lie, I probably couldn’t replicate this with any confidence on Ubuntu either.

If you’d like to try running NixOS on WSL2, please take a look at my nixos-wsl-starter template which should have you up and running with a useable terminal-powered development environment in less than 10 minutes.

There is even a video you can follow along with step by step.

If you have any questions you can reach out to me on Twitter and Mastodon.

If you’re interested in what I read to come up with solutions like this one, you can subscribe to my Software Development RSS feed.

If you’d like to watch me writing code while explaining what I’m doing, you can also subscribe to my YouTube channel.