Public/Publish-Fido2SshKey.ps1
|
function Publish-Fido2SshKey { <# .SYNOPSIS Publishes a FIDO2 SSH public key to a Linux host's authorized_keys over SSH. .DESCRIPTION Connects to the SSH-style `<user>@<host>` destination and writes the contents of `-PublicKeyPath` (or an auto-detected `id_*_sk_rk*.pub` in `%USERPROFILE%\.ssh`) to `~/.ssh/authorized_keys`. By default the key is appended only if it isn't already present. .PARAMETER Destination SSH-style target in the form `<user>@<host>`, e.g. `azureuser@10.0.0.4` or `ubuntu@server.example.com`. The host part may be a DNS name, an IPv4 address, or a bracketed IPv6 address (`user@[2001:db8::1]`). .PARAMETER PublicKeyPath Optional. Specific `.pub` file. If omitted, auto-detects FIDO2 keys in `%USERPROFILE%\.ssh` and prompts when multiple match. .PARAMETER Port SSH port. Defaults to 22. .PARAMETER IdentityFile Optional. Path to an existing SSH private key to authenticate the bootstrap connection with (passed to ssh as `-i`). Useful for publishing a new FIDO2 key on a host where you already have another key-based login. .PARAMETER WipeExistingKeys Replace `authorized_keys` with this key only. Lockout risk. .PARAMETER AllowDuplicate Append even if the key is already present (skip dedupe check). .EXAMPLE Publish-Fido2SshKey azureuser@server.example.com .EXAMPLE Publish-Fido2SshKey azureuser@131.123.32.3 -IdentityFile ~/.ssh/id_rsa .EXAMPLE Publish-Fido2SshKey ubuntu@10.0.0.4 -Port 2222 -WipeExistingKeys #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] param( [Parameter(Mandatory = $true, Position = 0)] [Alias('Target', 'HostName')] [ValidatePattern('^[^@\s]+@.+$')] [string]$Destination, [string]$PublicKeyPath, [int]$Port = 22, [Alias('i')] [string]$IdentityFile, [switch]$WipeExistingKeys, [switch]$AllowDuplicate ) # Split <user>@<host>; rsplit on '@' so usernames containing '@' (rare) still work. $atIndex = $Destination.LastIndexOf('@') $UserName = $Destination.Substring(0, $atIndex) $HostName = $Destination.Substring($atIndex + 1) if ([string]::IsNullOrWhiteSpace($UserName) -or [string]::IsNullOrWhiteSpace($HostName)) { throw "Destination '$Destination' is not in the expected <user>@<host> format." } if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) { throw "ssh was not found. Install the OpenSSH client (on Windows: the OpenSSH Client capability; on Linux/macOS: the `openssh-client` / `openssh` package)." } if ([string]::IsNullOrWhiteSpace($PublicKeyPath)) { $PublicKeyPath = Resolve-Fido2PublicKeyPath -SshDirectory (Get-Fido2DefaultSshDirectory) } if (-not (Test-Path -LiteralPath $PublicKeyPath -PathType Leaf)) { throw "Public key file was not found: $PublicKeyPath" } $keyLine = (Get-Content -LiteralPath $PublicKeyPath -Raw).Trim() if ([string]::IsNullOrWhiteSpace($keyLine)) { throw "Public key file is empty: $PublicKeyPath" } if ($WipeExistingKeys) { $remoteCommand = @' mkdir -p ~/.ssh chmod 700 ~/.ssh cat > ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys '@ } elseif ($AllowDuplicate) { $remoteCommand = @' mkdir -p ~/.ssh chmod 700 ~/.ssh cat >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys '@ } else { $remoteCommand = @' mkdir -p ~/.ssh chmod 700 ~/.ssh key="$(cat)" touch ~/.ssh/authorized_keys if ! grep -qxF "$key" ~/.ssh/authorized_keys 2>/dev/null; then printf '%s\n' "$key" >> ~/.ssh/authorized_keys fi chmod 600 ~/.ssh/authorized_keys '@ } # Ensure Linux shell receives LF line endings; CRLF can break if/then/fi parsing. $remoteCommand = $remoteCommand -replace "`r`n", "`n" $target = "$UserName@$HostName" $sshArgs = @() if ($Port -ne 22) { $sshArgs += @("-p", $Port) } if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) { $resolvedIdentity = (Resolve-Path -LiteralPath $IdentityFile -ErrorAction SilentlyContinue) if (-not $resolvedIdentity) { throw "Identity file was not found: $IdentityFile" } # IdentitiesOnly=yes makes ssh ignore agent / default keys and use this one only. $sshArgs += @('-o', 'IdentitiesOnly=yes', '-i', $resolvedIdentity.Path) } $sshArgs += @($target, $remoteCommand) $actionDescription = "Publish SSH public key from '$PublicKeyPath' to $target" if ($WipeExistingKeys) { $actionDescription += " (wipe existing authorized_keys first)" } if ($PSCmdlet.ShouldProcess($target, $actionDescription)) { # Send the local public key line on stdin so the remote shell can write it safely. $keyLine | & ssh @sshArgs if ($LASTEXITCODE -ne 0) { throw "ssh command failed with exit code $LASTEXITCODE." } } Write-Host "Key published to $target using $PublicKeyPath" if ($WipeExistingKeys) { Write-Host "Existing remote authorized_keys entries were replaced." } else { Write-Host "Existing remote authorized_keys entries were preserved." } } |