Skip to content

Instantly share code, notes, and snippets.

@MaxwellDPS
Last active October 30, 2025 00:54
Show Gist options
  • Select an option

  • Save MaxwellDPS/ea765e44384b90dffd12181667979565 to your computer and use it in GitHub Desktop.

Select an option

Save MaxwellDPS/ea765e44384b90dffd12181667979565 to your computer and use it in GitHub Desktop.
WSL Yubikey attach helper

YubiKey WSL Management - Usage Guide

Quick Start

Basic Operations

# Attach first YubiKey found (most common use case)
.\Connect-YubiKeyToWSL.ps1

# Detach YubiKey
.\Connect-YubiKeyToWSL.ps1 -Detach

# See what would happen without making changes
.\Connect-YubiKeyToWSL.ps1 -WhatIf

Working with Multiple Devices

# List all YubiKeys (dry run shows what would be attached)
.\Connect-YubiKeyToWSL.ps1 -WhatIf -All

# Attach all YubiKeys
.\Connect-YubiKeyToWSL.ps1 -All

# Attach specific YubiKey model
.\Connect-YubiKeyToWSL.ps1 -VendorId 1050 -ProductId 0407

Advanced Usage

# Attach by specific bus ID
.\Connect-YubiKeyToWSL.ps1 -BusId "20-3"

# Force attachment without confirmations
.\Connect-YubiKeyToWSL.ps1 -Force

# Verbose output for troubleshooting
.\Connect-YubiKeyToWSL.ps1 -Verbose

# Combine options
.\Connect-YubiKeyToWSL.ps1 -All -Force -Verbose

Installation

Prerequisites

  1. Install usbipd-win

    winget install --exact dorssel.usbipd-win
  2. Install WSL USB support in Linux

    # In your WSL distribution:
    sudo apt update
    sudo apt install linux-tools-virtual hwdata
    sudo update-alternatives --install /usr/local/bin/usbip usbip $(ls /usr/lib/linux-tools/*/usbip | tail -n1) 20
  3. Configure permissions (first time only)

    • Run PowerShell as Administrator
    • Attach your YubiKey once manually
    • Future attachments won't require admin rights

Script Installation

# Download script
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/yourusername/yubikey-wsl/main/Connect-YubiKeyToWSL.ps1" -OutFile "$HOME\Scripts\Connect-YubiKeyToWSL.ps1"

# Or clone repository
git clone https://github.com/yourusername/yubikey-wsl.git
cd yubikey-wsl

Troubleshooting

Common Issues

"No YubiKey devices found"

# Check all USB devices
usbipd list

# Check JSON output
usbipd state | ConvertFrom-Json | Select-Object -ExpandProperty Devices

# Try without filters
.\Connect-YubiKeyToWSL.ps1 -BusId "20-3"  # Use actual BusId from list

"Device already attached"

# Force re-attachment
.\Connect-YubiKeyToWSL.ps1 -Force

# Or manually detach first
.\Connect-YubiKeyToWSL.ps1 -Detach
.\Connect-YubiKeyToWSL.ps1

"Access denied" errors

# Run as Administrator (first time only)
Start-Process powershell -Verb RunAs -ArgumentList "-File `"$PWD\Connect-YubiKeyToWSL.ps1`""

Device not working in WSL after attach

# In WSL, check if device is visible
lsusb

# Check YubiKey specifically
ykman list

# May need to restart services
sudo systemctl restart pcscd

Debugging

# Maximum verbosity
$VerbosePreference = "Continue"
.\Connect-YubiKeyToWSL.ps1 -Verbose

# Check script's view of devices
.\Connect-YubiKeyToWSL.ps1 -WhatIf -Verbose

# Manual state inspection
$state = usbipd state | ConvertFrom-Json
$state.Devices | Where-Object { $_.InstanceId -match "1050" } | Format-List

Integration Examples

Alias for Quick Access

Add to your PowerShell profile:

# $PROFILE
Set-Alias yubikey "$HOME\Scripts\Connect-YubiKeyToWSL.ps1"
Set-Alias yk "$HOME\Scripts\Connect-YubiKeyToWSL.ps1"

# Usage
yk              # Attach
yk -Detach      # Detach
yk -WhatIf      # Check status

Scheduled Task for Auto-Attach

# Create scheduled task to attach on login
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-WindowStyle Hidden -File `"$HOME\Scripts\Connect-YubiKeyToWSL.ps1`" -Force"

$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME

Register-ScheduledTask -TaskName "AttachYubiKeyToWSL" `
    -Action $action -Trigger $trigger `
    -Description "Automatically attach YubiKey to WSL at login"

WSL Startup Script

# In WSL ~/.bashrc or ~/.zshrc
check_yubikey() {
    if ! lsusb | grep -q "Yubico"; then
        echo "YubiKey not attached. Run 'yk' in Windows to attach."
    else
        echo "YubiKey detected ✓"
    fi
}

# Check on shell startup
check_yubikey

Security Notes

  1. First attachment requires admin rights - This is a Windows security feature
  2. Subsequent attachments work without elevation - Windows remembers the device
  3. Each WSL distribution gets isolated USB access - Devices are attached to all WSL2 instances
  4. Detaching doesn't remove Windows drivers - The device returns to Windows normally

Performance Tips

  • Use specific filters (-VendorId, -ProductId) for faster execution
  • The -All flag processes devices in parallel when possible
  • JSON parsing is faster than text parsing (old script version)
  • Cache device state when polling frequently

Exit Codes

Code Meaning Example Scenario
0 Success All operations completed
1 Not Found No matching devices or missing prerequisites
2 Partial Failure Some devices attached, some failed
3 Total Failure All operations failed

Version History

  • v2.0.0 - Complete rewrite with JSON parsing, WhatIf support, proper error handling
  • v1.0.0 - Original text-parsing version

Contributing

Suggestions and improvements welcome! The script is designed to be extended:

  • Add new device types to $Config.KnownYubicoVendorIds
  • Customize retry logic in $Config.RetryAttempts
  • Add pre/post attachment hooks
  • Extend the UsbDevice class with new properties
#Requires -Version 5.1
<#
.SYNOPSIS
Manage YubiKey (or other USB device) connections to WSL using usbipd.
.DESCRIPTION
Robustly attaches or detaches YubiKey devices to a running WSL instance via usbipd-win.
Uses JSON output from usbipd for reliable parsing and handles USB re-enumeration.
Verifies successful attachment by checking device visibility in WSL with lsusb.
Exit Codes:
0 - Success
1 - No devices found or prerequisites missing
2 - Some operations failed
3 - All operations failed
.PARAMETER VendorId
Optional 4-digit hex USB vendor ID (e.g. 1050 for Yubico).
.PARAMETER ProductId
Optional 4-digit hex USB product ID (e.g. 0407).
.PARAMETER BusId
One or more explicit bus IDs (e.g. 20-3,21-2). Bypasses VID/PID filtering.
.PARAMETER Detach
If present, detaches devices instead of attaching them.
.PARAMETER All
Process all matching devices. Without this, only the first device is processed.
.PARAMETER WhatIf
Shows what would be done without making changes.
.PARAMETER Force
Skip confirmation prompts and prerequisite warnings.
.PARAMETER NoVerify
Skip verification of device attachment in WSL (lsusb check).
.EXAMPLE
# Attach first YubiKey found
.\Connect-YubiKeyToWSL.ps1
.EXAMPLE
# Attach specific YubiKey 5 NFC
.\Connect-YubiKeyToWSL.ps1 -VendorId 1050 -ProductId 0407
.EXAMPLE
# Detach all YubiKeys
.\Connect-YubiKeyToWSL.ps1 -Detach -All
.EXAMPLE
# Check what would be attached without doing it
.\Connect-YubiKeyToWSL.ps1 -WhatIf
.EXAMPLE
# Attach device by specific bus ID
.\Connect-YubiKeyToWSL.ps1 -BusId "20-3"
.EXAMPLE
# Attach without verification
.\Connect-YubiKeyToWSL.ps1 -NoVerify
.NOTES
Requires:
- usbipd-win installed (https://github.com/dorssel/usbipd-win)
- WSL2 with a running distribution
- Administrator privileges for first-time device attachment
- lsusb installed in WSL for verification (usually in usbutils package)
Author: Enhanced version based on community script
Version: 2.1.0
#>
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Filter')]
param(
[Parameter(ParameterSetName='Filter')]
[ValidatePattern('^[0-9a-fA-F]{4}$')]
[string]$VendorId,
[Parameter(ParameterSetName='Filter')]
[ValidatePattern('^[0-9a-fA-F]{4}$')]
[string]$ProductId,
[Parameter(ParameterSetName='BusId', Mandatory)]
[ValidatePattern('^[0-9]+-[0-9]+$')]
[string[]]$BusId,
[switch]$Detach,
[switch]$All,
[switch]$Force,
[switch]$NoVerify
)
# ============================================================================
# Configuration
# ============================================================================
# Known YubiKey vendor IDs
$script:Config = @{
KnownYubicoVendorIds = @('1050', '2581', '1500')
RetryAttempts = 5
RetryDelayMs = 500
RequiredCommands = @('usbipd', 'wsl')
VerificationRetries = 3
VerificationDelayMs = 1000
}
# Exit codes
enum ExitCode {
Success = 0
NotFound = 1
PartialFailure = 2
TotalFailure = 3
}
# Device state
enum DeviceState {
Unknown
Detached
Attached
AttachedToOther
Error
}
# ============================================================================
# Classes
# ============================================================================
class UsbDevice {
[string]$BusId
[string]$VendorId
[string]$ProductId
[string]$Description
[string]$InstanceId
[string]$ClientIPAddress
[bool]$IsForced
[string]$PersistedGuid
[DeviceState]$State
UsbDevice([PSCustomObject]$json) {
$this.BusId = $json.BusId
$this.Description = $json.Description
$this.InstanceId = $json.InstanceId
$this.ClientIPAddress = $json.ClientIPAddress
$this.IsForced = $json.IsForced
$this.PersistedGuid = $json.PersistedGuid
# Parse VID/PID from InstanceId (e.g., "USB\VID_1050&PID_0407\...")
if ($json.InstanceId -match 'VID_([0-9A-F]{4})&PID_([0-9A-F]{4})') {
$this.VendorId = $Matches[1]
$this.ProductId = $Matches[2]
}
# Determine state
if ($json.ClientIPAddress) {
$this.State = [DeviceState]::Attached
} else {
$this.State = [DeviceState]::Detached
}
}
[bool] MatchesFilter([string]$vendorId, [string]$productId) {
if ($vendorId -and $this.VendorId -ne $vendorId) { return $false }
if ($productId -and $this.ProductId -ne $productId) { return $false }
return $true
}
[bool] IsYubiKey() {
return $this.VendorId -in $script:Config.KnownYubicoVendorIds -or
$this.Description -match 'YubiKey|Yubico'
}
[string] ToString() {
$vidStr = if ($this.VendorId) { $this.VendorId } else { '????' }
$pidStr = if ($this.ProductId) { $this.ProductId } else { '????' }
return "$($this.BusId) [$vidStr`:$pidStr] $($this.Description)"
}
[string] GetUsbIdString() {
# Returns the VID:PID format used by lsusb (lowercase)
$vidStr = if ($this.VendorId) { $this.VendorId.ToLower() } else { '????' }
$pidStr = if ($this.ProductId) { $this.ProductId.ToLower() } else { '????' }
return "${vidStr}:${pidStr}"
}
}
# ============================================================================
# Helper Functions
# ============================================================================
function Test-Prerequisites {
<#
.SYNOPSIS
Checks if all required tools are available
#>
[CmdletBinding()]
param()
$issues = @()
# Check for required commands
foreach ($cmd in $script:Config.RequiredCommands) {
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
$issues += "$cmd is not installed or not in PATH"
}
}
# Check if WSL is running
if (Get-Command wsl -ErrorAction SilentlyContinue) {
$wslStatus = wsl --list --running 2>$null
if (-not $wslStatus -or $wslStatus -match 'no running distributions') {
$issues += "No WSL distributions are running. Start WSL first."
}
}
# Check for elevation (warning only)
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Warning "Not running as Administrator. You may need elevation for first-time device attachment."
}
if ($issues.Count -gt 0) {
foreach ($issue in $issues) {
Write-Error $issue
}
return $false
}
return $true
}
function Test-DeviceInWsl {
<#
.SYNOPSIS
Verifies if a device is visible in WSL using lsusb
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[UsbDevice]$Device
)
Write-Verbose "Verifying device visibility in WSL (VID:$($Device.VendorId) PID:$($Device.ProductId))..."
try {
# Run lsusb in WSL and capture output
$lsusbOutput = wsl.exe lsusb 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Warning "lsusb command failed. Ensure usbutils is installed in WSL (apt install usbutils)"
return $false
}
# Look for the device by VID:PID
$usbId = $Device.GetUsbIdString()
$deviceFound = $lsusbOutput | Select-String -Pattern $usbId -Quiet
if ($deviceFound) {
# Get the specific line for better feedback
$deviceLine = $lsusbOutput | Select-String -Pattern $usbId | Select-Object -First 1
Write-Verbose "Device found in WSL: $deviceLine"
return $true
} else {
Write-Verbose "Device $usbId not found in lsusb output"
return $false
}
}
catch {
Write-Warning "Error checking device in WSL: $_"
return $false
}
}
function Get-UsbDevices {
<#
.SYNOPSIS
Gets USB devices from usbipd using JSON output
#>
[CmdletBinding()]
param(
[string]$VendorId,
[string]$ProductId,
[string[]]$BusId
)
Write-Verbose "Retrieving USB devices from usbipd..."
try {
$jsonOutput = usbipd state 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to get device list from usbipd"
return @()
}
$state = $jsonOutput | ConvertFrom-Json
$devices = $state.Devices | ForEach-Object { [UsbDevice]::new($_) }
Write-Verbose "Found $($devices.Count) total USB devices"
# Apply filters
if ($BusId) {
$devices = $devices | Where-Object { $_.BusId -in $BusId }
Write-Verbose "Filtered to $($devices.Count) devices by BusId"
} elseif ($VendorId -or $ProductId) {
$devices = $devices | Where-Object { $_.MatchesFilter($VendorId, $ProductId) }
Write-Verbose "Filtered to $($devices.Count) devices by VID/PID"
} else {
# Default: filter to YubiKeys only
$devices = $devices | Where-Object { $_.IsYubiKey() }
Write-Verbose "Filtered to $($devices.Count) YubiKey devices"
}
return $devices
}
catch {
Write-Error "Error parsing usbipd output: $_"
return @()
}
}
function Wait-ForDeviceReenumeration {
<#
.SYNOPSIS
Waits for a device to reappear after detach with new BusId
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[UsbDevice]$OriginalDevice,
[int]$MaxAttempts = 10,
[int]$InitialDelayMs = 1000,
[int]$DelayMs = 500
)
Write-Verbose "Waiting for device re-enumeration (VID:$($OriginalDevice.VendorId) PID:$($OriginalDevice.ProductId))..."
# Initial delay to allow Windows to process the detach
Start-Sleep -Milliseconds $InitialDelayMs
for ($i = 0; $i -lt $MaxAttempts; $i++) {
$devices = Get-UsbDevices -VendorId $OriginalDevice.VendorId -ProductId $OriginalDevice.ProductId
# Look for any device with matching VID/PID that's not attached
$availableDevice = $devices | Where-Object {
$_.State -ne [DeviceState]::Attached -and
$_.VendorId -eq $OriginalDevice.VendorId -and
$_.ProductId -eq $OriginalDevice.ProductId
} | Select-Object -First 1
if ($availableDevice) {
if ($availableDevice.BusId -ne $OriginalDevice.BusId) {
Write-Information "Device re-enumerated: $($OriginalDevice.BusId) → $($availableDevice.BusId)" -InformationAction Continue
}
return $availableDevice
}
# Also check if original device is back but detached
$originalBack = $devices | Where-Object {
$_.BusId -eq $OriginalDevice.BusId -and
$_.State -eq [DeviceState]::Detached
} | Select-Object -First 1
if ($originalBack) {
Write-Verbose "Original device found at same BusId"
return $originalBack
}
Write-Verbose "Attempt $($i+1)/$MaxAttempts - Device not found yet (found $($devices.Count) devices)"
Start-Sleep -Milliseconds $DelayMs
}
Write-Warning "Device did not reappear after $MaxAttempts attempts"
return $null
}
function Invoke-UsbDeviceAttach {
<#
.SYNOPSIS
Attaches a USB device to WSL
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[UsbDevice]$Device
)
process {
if ($Device.State -eq [DeviceState]::Attached) {
Write-Information "Device already attached: $($Device.ToString())" -InformationAction Continue
if ($Force -or $PSCmdlet.ShouldContinue("Device is already attached. Detach and re-attach?", "Re-attach Device")) {
Write-Verbose "Detaching device for re-attachment..."
$detachResult = Invoke-UsbDeviceDetach -Device $Device
if (-not $detachResult) {
Write-Error "Failed to detach device for re-attachment"
return $false
}
# Wait for re-enumeration
$newDevice = Wait-ForDeviceReenumeration -OriginalDevice $Device
if (-not $newDevice) {
Write-Error "Failed to find device after detach. The device may need to be manually reconnected."
return $false
}
$Device = $newDevice
} else {
return $true
}
}
if ($PSCmdlet.ShouldProcess($Device.ToString(), "Attach to WSL")) {
Write-Verbose "Executing: usbipd attach --wsl --busid $($Device.BusId)"
$output = usbipd attach --wsl --busid $Device.BusId 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Information "✓ Attached: $($Device.ToString())" -InformationAction Continue
# Verify device is visible in WSL unless -NoVerify is specified
if (-not $NoVerify) {
Write-Information " Verifying device in WSL..." -InformationAction Continue
# Try verification with retries
$verified = $false
for ($i = 0; $i -lt $script:Config.VerificationRetries; $i++) {
if ($i -gt 0) {
Start-Sleep -Milliseconds $script:Config.VerificationDelayMs
}
if (Test-DeviceInWsl -Device $Device) {
$verified = $true
Write-Information " ✓ Device verified in WSL (visible to lsusb)" -InformationAction Continue
break
}
if ($i -lt ($script:Config.VerificationRetries - 1)) {
Write-Verbose "Verification attempt $($i+1) failed, retrying..."
}
}
if (-not $verified) {
Write-Warning " Device attached but not visible in WSL. Check if usbutils is installed (apt install usbutils)"
Write-Warning " You can manually verify with: wsl.exe lsusb | grep -i $($Device.GetUsbIdString())"
}
}
return $true
} elseif ($output -match 'already attached') {
# Handle race condition where device shows as detached but is actually attached
Write-Information "Device reports as already attached: $($Device.ToString())" -InformationAction Continue
# Still verify if not skipped
if (-not $NoVerify -and (Test-DeviceInWsl -Device $Device)) {
Write-Information " ✓ Device verified in WSL" -InformationAction Continue
}
return $true
} else {
Write-Error "Failed to attach device: $output"
return $false
}
}
return $true # Dry run
}
}
function Invoke-UsbDeviceDetach {
<#
.SYNOPSIS
Detaches a USB device from WSL
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[UsbDevice]$Device
)
process {
if ($Device.State -eq [DeviceState]::Detached) {
Write-Verbose "Device already detached: $($Device.ToString())"
return $true
}
if ($PSCmdlet.ShouldProcess($Device.ToString(), "Detach from WSL")) {
Write-Verbose "Executing: usbipd detach --busid $($Device.BusId)"
$output = usbipd detach --busid $Device.BusId 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Information "✓ Detached: $($Device.ToString())" -InformationAction Continue
# Optionally verify device is no longer visible in WSL
if (-not $NoVerify) {
Start-Sleep -Milliseconds 500 # Brief delay for detach to complete
if (-not (Test-DeviceInWsl -Device $Device)) {
Write-Information " ✓ Device verified as detached from WSL" -InformationAction Continue
} else {
Write-Warning " Device may still be visible in WSL"
}
}
return $true
} elseif ($output -match 'not attached|no client') {
Write-Verbose "Device was not attached: $($Device.ToString())"
return $true
} else {
Write-Error "Failed to detach device: $output"
return $false
}
}
return $true # Dry run
}
}
function Show-WslUsbDevices {
<#
.SYNOPSIS
Shows all USB devices currently visible in WSL
#>
[CmdletBinding()]
param()
Write-Information "`nUSB devices currently visible in WSL:" -InformationAction Continue
try {
$lsusbOutput = wsl.exe lsusb 2>$null
if ($LASTEXITCODE -eq 0 -and $lsusbOutput) {
$lsusbOutput | ForEach-Object {
Write-Information " $_" -InformationAction Continue
}
} else {
Write-Information " (No devices found or lsusb not available)" -InformationAction Continue
}
}
catch {
Write-Warning "Could not list USB devices in WSL: $_"
}
}
# ============================================================================
# Main Script Logic
# ============================================================================
# Check prerequisites
if (-not (Test-Prerequisites)) {
if (-not $Force) {
exit [ExitCode]::NotFound
}
Write-Warning "Continuing despite missing prerequisites (Force mode)"
}
# Get matching devices
$devices = Get-UsbDevices -VendorId $VendorId -ProductId $ProductId -BusId $BusId
if ($devices.Count -eq 0) {
if ($BusId) {
Write-Error "No devices found with BusId(s): $($BusId -join ', ')"
} elseif ($VendorId -and $ProductId) {
Write-Error "No devices found with VID:$VendorId PID:$ProductId"
} elseif ($VendorId) {
Write-Error "No devices found with VID:$VendorId"
} elseif ($ProductId) {
Write-Error "No devices found with PID:$ProductId"
} else {
Write-Error "No YubiKey devices found"
}
exit [ExitCode]::NotFound
}
# Display found devices
Write-Information "Found $($devices.Count) matching device(s):" -InformationAction Continue
foreach ($device in $devices) {
$status = if ($device.State -eq [DeviceState]::Attached) { "[Attached]" } else { "[Detached]" }
Write-Information " $status $($device.ToString())" -InformationAction Continue
}
# Select devices to process
$devicesToProcess = if ($All) { $devices } else { $devices | Select-Object -First 1 }
if (-not $All -and $devices.Count -gt 1) {
Write-Information "Processing first device only. Use -All to process all devices." -InformationAction Continue
}
# Process devices
$results = @{
Success = 0
Failed = 0
}
foreach ($device in $devicesToProcess) {
$success = if ($Detach) {
Invoke-UsbDeviceDetach -Device $device
} else {
Invoke-UsbDeviceAttach -Device $device
}
if ($success) {
$results.Success++
} else {
$results.Failed++
}
}
# Summary
Write-Information "" -InformationAction Continue
$action = if ($Detach) { "Detached" } else { "Attached" }
Write-Information "Summary: $action $($results.Success), Failed $($results.Failed)" -InformationAction Continue
# Optionally show all USB devices in WSL after attachment
if (-not $Detach -and $results.Success -gt 0 -and -not $NoVerify) {
Show-WslUsbDevices
}
# Exit with appropriate code
if ($results.Failed -eq 0) {
exit [ExitCode]::Success
} elseif ($results.Success -gt 0) {
exit [ExitCode]::PartialFailure
} else {
exit [ExitCode]::TotalFailure
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment