#Requires -Version 5.1 <# .SYNOPSIS Windows port of ssh-copy-id — installs your public key on a remote host. .DESCRIPTION Automatically detects whether the remote host is Windows or Linux/Mac via SSH banner (no authentication required), then installs the public key into the correct authorized_keys location. Windows targets: writes to both %USERPROFILE%\.ssh\authorized_keys and C:\ProgramData\ssh\administrators_authorized_keys (if the remote user is a local administrator), then fixes file permissions via icacls. Linux/Mac targets: appends to ~/.ssh/authorized_keys and sets chmod 600. .PARAMETER Target Remote host in [user@]host format. .PARAMETER i Path to the public key file. Defaults to the first found key among id_ed25519, id_rsa, id_ecdsa, id_dsa in %USERPROFILE%\.ssh\. .PARAMETER p SSH port. Defaults to 22. .EXAMPLE ssh-copy-id user@192.168.0.100 ssh-copy-id -i ~/.ssh/id_ed25519.pub user@hostname ssh-copy-id -p 2222 user@hostname .NOTES Requires OpenSSH client (ssh, ssh-keyscan) to be available in PATH. On Windows 10/11 this is bundled; install via: Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 #>
# --------------------------------------------------------------------------- # Detect remote OS via SSH banner (requires no authentication) # Windows : "SSH-2.0-OpenSSH_for_Windows_x.x" # Linux : "SSH-2.0-OpenSSH_8.x Ubuntu-..." # --------------------------------------------------------------------------- $banner = (ssh-keyscan-p$p-T5$RemoteHost2>$null) -join" " if (-not$banner) { Write-Warning"ssh-keyscan got no response from ${RemoteHost}:$p — defaulting to Linux/Mac." } $isWindows = $banner-match"OpenSSH_for_Windows" Write-Host"Remote OS: $(if ($isWindows) { 'Windows' } else { 'Linux/Unix/Mac' })"
# TODO: support Xray/custom SSH servers whose banners don't match standard patterns
Write-Host"Connecting to $RemoteHost (you may be prompted for a password)..."
# --------------------------------------------------------------------------- # Install key # --------------------------------------------------------------------------- if ($isWindows) { # Encode the public key as base64 so it survives the SSH → cmd → PowerShell # quoting gauntlet without any escaping issues. $keyB64 = [Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes($pubkey) )
$remoteScript = @" `$key = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$keyB64')) `$dir = "`$env:USERPROFILE\.ssh" New-Item -ItemType Directory -Force -Path `$dir | Out-Null # User-level authorized_keys `$ak = `$dir + '\authorized_keys' `$lines = if (Test-Path `$ak) { [IO.File]::ReadAllLines(`$ak) } else { @() } if (`$lines -notcontains `$key) { Add-Content `$ak `$key Write-Host 'KEY_ADDED_USER' } else { Write-Host 'KEY_EXISTS_USER' } # Administrator-level authorized_keys (required when remote user is a local admin) `$adminAk = 'C:\ProgramData\ssh\administrators_authorized_keys' `$adminList = (net localgroup administrators 2>`$null) -join '' if (`$adminList -match [regex]::Escape(`$env:USERNAME)) { `$l2 = if (Test-Path `$adminAk) { [IO.File]::ReadAllLines(`$adminAk) } else { @() } if (`$l2 -notcontains `$key) { Add-Content `$adminAk `$key } icacls `$adminAk /inheritance:r /grant 'SYSTEM:(F)' /grant 'BUILTIN\Administrators:(F)' | Out-Null Write-Host 'KEY_ADDED_ADMIN' } # TODO: fix permissions on authorized_keys for non-admin users as well # TODO: handle Microsoft-account usernames (contain backslash) in admin check "@
# Encode script as UTF-16LE base64 for powershell -EncodedCommand (no quoting issues) $encoded = [Convert]::ToBase64String( [System.Text.Encoding]::Unicode.GetBytes($remoteScript) ) $result = (ssh -p$p-o StrictHostKeyChecking=no ` "${RemoteUser}@${RemoteHost}""powershell -EncodedCommand $encoded") -join"`n"
# --------------------------------------------------------------------------- # TODO (future improvements) # --------------------------------------------------------------------------- # [ ] -n / --dry-run flag: show what would be done without modifying anything # [ ] support Ed25519 key generation hint when no key exists # [ ] -f flag: force overwrite even if key exists (refresh comment/type) # [ ] support SSH config file aliases as Target (resolve via ssh -G) # [ ] support ProxyJump hosts (pass -J flag through to ssh-keyscan and ssh) # [ ] verify key was actually installed by doing a BatchMode=yes test connect # [ ] publish as a PowerShell Gallery module (Install-Module ssh-copy-id)