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 `-UserName`@`-HostName` over SSH 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 HostName
        DNS name or IP of the target host.
 
    .PARAMETER UserName
        Linux user whose `authorized_keys` to update.
 
    .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 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 -HostName server.example.com -UserName azureuser
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    param(
        [Parameter(Mandatory = $true)][string]$HostName,
        [Parameter(Mandatory = $true)][string]$UserName,
        [string]$PublicKeyPath,
        [int]$Port = 22,
        [switch]$WipeExistingKeys,
        [switch]$AllowDuplicate
    )

    if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) {
        throw "ssh was not found. Install the OpenSSH Client Windows feature first."
    }

    if ([string]::IsNullOrWhiteSpace($PublicKeyPath)) {
        $PublicKeyPath = Resolve-Fido2PublicKeyPath -SshDirectory (Join-Path $env:USERPROFILE ".ssh")
    }
    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) }
    $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."
    }
}