Last active
December 4, 2023 21:40
-
-
Save Scherlac/a486301725b6ee18876c4f7a81cc6de9 to your computer and use it in GitHub Desktop.
This is a powershell script to copy the ssh key to a remote host
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
This is a powershell script to copy the ssh key to a remote host | |
.DESCRIPTION | |
This is a powershell script to copy the ssh key to a remote host | |
The script will: | |
- update the config file with the host alias and key file | |
- generate a new ssh key if it does not exist | |
- copy the key to the remote host | |
- show the key file location | |
- show a command example to connect to the remote host | |
.EXAMPLE | |
We can use the script to copy the key to a remote host as follows: | |
Linux host: | |
> . .\ssh-copy.ps1 | |
> Copy-SSHKey -RemoteHost myhost -RemoteUser myuser | |
Windows host: | |
> . .\ssh-copy.ps1 | |
> Copy-SSHKey -RemoteHost myhost -RemoteUser myuser -RemoteOS windows [-AdminUser] | |
.NOTES | |
**Note**: Not all part of the script are tested, use it at your own risk, please report any issue or suggestion | |
**Note**: The script requires powershell 7.3 or higher (we use "?" operator (ternary operator)) | |
**Note**: The earlier versions of powershell and openssh for Windows also has an issue handling commands with arguments | |
containing double quotes. | |
See: | |
- [Remote SSH commands require double escaping before hitting the DefaultShell](https://github.com/PowerShell/Win32-OpenSSH/issues/1082) | |
- [PowerShell stripping double quotes from command line arguments](https://stackoverflow.com/a/12888757/5770014) | |
**Note**: The file format for the ssh config and authorized_keys files is LF (Unix), with UTF-8 encoding may work with BOM. | |
For this script to work properly, the file format of this script must be LF (Unix) as well. | |
See: | |
- [Using PowerShell to write a file in UTF-8 without the BOM](https://stackoverflow.com/a/5596984/5770014) | |
Install PowerShell 7.3 or higher: | |
- [Installing PowerShell on Windows](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) | |
- [Installing OpenSSH on Windows](https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=powershell#install-openssh-for-windows) | |
- [OpenSSH GitHub repository](https://github.com/PowerShell/Win32-OpenSSH) | |
#> | |
function Copy-SSHKey { | |
param ( | |
# The remote host name or IP address | |
[Parameter(Mandatory=$true)] | |
[string]$RemoteHost, | |
# The port number | |
[Parameter(Mandatory=$false)] | |
[int]$Port = 22, | |
# Host alias, host name or host pattern to match used in the config file | |
# by default we use the remote host name without domain name as alias | |
[Parameter(Mandatory=$false)] | |
[string]$Alias = ($RemoteHost -replace '\..*',''), | |
# The remote user | |
[Parameter(Mandatory=$true)] | |
[string]$RemoteUser, | |
# The home folder, by default we use the $env:HOME or $env:USERPROFILE environment variable | |
[Parameter(Mandatory=$false)] | |
$HomeFolder = ( _IF $env:HOME $env:HOME $env:USERPROFILE ), | |
# The key file name, by default we use the user profile folder and the alias | |
[Parameter(Mandatory=$false)] | |
[string]$KeyFile = ("$HomeFolder/.ssh/id_rsa_$( $Alias -replace '[^a-zA-Z0-9]','')"), | |
# Key comment | |
[Parameter(Mandatory=$false)] | |
[string]$KeyComment = "$RemoteUser@$RemoteHost", | |
# The encryption type for the key file (rsa2048, rsa4096, ed25519) | |
[Parameter(Mandatory=$false)] | |
[ValidateSet('rsa2048', 'rsa4096','ed25519')] | |
[string]$KeyType = 'ed25519', | |
# The remote host operating system (linux, windows) | |
[Parameter(Mandatory=$false)] | |
[ValidateSet('linux', 'windows')] | |
[string]$RemoteOS = 'linux', | |
# The update method for the remote host authorized_keys file (append, replace) | |
[Parameter(Mandatory=$false)] | |
[ValidateSet('append', 'replace')] | |
[string]$UpdateMethod = 'replace', | |
# Specify this flag if the remote host is windows and the user is an administrator | |
[switch]$AdminUser, | |
# Flag to enable debug mode | |
[switch]$Trace, | |
# Flag to force to generate new key file even if it exists | |
[switch]$ForceGenerate, | |
# Flag to force to upload the key even if no new key is generated | |
[switch]$ForceUpload | |
) | |
$supportedPowershel = ( 100 * $PSVersionTable.PSVersion.Major + $PSVersionTable.PSVersion.Minor ) -ge 703 | |
if (-not $supportedPowershel) { | |
Write-Warning "This script requires powershell 7.3 or higher in local host" | |
# ask for confirmation to continue | |
Write-Host "Press any key to continue or CTRL+C to cancel" | |
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | |
Start-Sleep -Seconds 2 | |
} | |
# Get the current ssh config file content | |
$configText = ( Get-Content -Raw "$HomeFolder/.ssh/config" ) | |
$configText = $configText -replace "`r`n", "`n" | |
# We replace the host alias if it exists with multiline regex | |
# | |
# SSH config fils description: https://linux.die.net/man/5/ssh_config | |
# | |
# NOTE: according to the documentation, the host alias section continues | |
# "up to the next Host keyword" but it does not work for the last host | |
# alias. So we expect all options for the same host alias are indented | |
# with at least one space | |
$regex = @" | |
(?mx) # multiline, ignore pattern whitespace | |
(?<hostAlias> | |
(^[ \t]*Host\s+$Alias\s*$\n) | |
((^[ \t]+[^\n]*$\n)|(^\#[^\n]*$\n)|(^\s*$\n))+ | |
) | |
"@ | |
# We create the new entry to append to the config file | |
# Adding some empty lines around the new entry to make it more readable | |
$newConfig = @" | |
`n`n | |
Host $Alias | |
HostName $RemoteHost | |
Port $Port | |
User $RemoteUser | |
IdentityFile $KeyFile | |
`n`n | |
"@ | |
If ($configText -match $regex) { | |
Write-Host "Updating the config file..." | |
$configText = $configText -replace $regex, $newConfig | |
} else { | |
Write-Host "Appending to the config file..." | |
$configText += $newConfig | |
} | |
# Removing empty lines not needed | |
$configText = $configText -replace "(\s*`n){3,}", "`n`n" | |
if ($Trace.IsPresent) { | |
# Show the config file for the user and ask for confirmation | |
Write-Host | |
Write-Host "The following text will be appended to the config file:" | |
Write-Host "---------------------------------------------------" | |
Write-Host $configText | |
Write-Host "---------------------------------------------------" | |
Write-Host "Press any key to continue or CTRL+C to cancel" | |
Write-Host | |
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | |
Start-Sleep -Seconds 2 | |
} | |
$configText | Set-Content -NoNewline "$HomeFolder/.ssh/config" | |
if (-not (Test-Path $KeyFile) -or $ForceGenerate.IsPresent) { | |
$type = _IF ($KeyType -like 'rsa*') 'rsa' 'ed25519' | |
$bits = _IF ($type -eq 'rsa') @( "-b", ($KeyType -replace 'rsa','') ) $null | |
Write-Host "Generating $type key..." | |
Write-Host "When prompted, please, provide a passphrase to encrypt the key OR press enter to skip" | |
# Generate the key based on the selected type | |
ssh-keygen -t $type $bits -f $KeyFile -C $KeyComment | |
$newKey = $true | |
} | |
if ($newKey -or $ForceUpload.IsPresent) { | |
$options = @() | |
$command = "" | |
# base on the remote OS we copy the key to the remote host | |
if ($RemoteOS -eq 'linux') { | |
Write-Host "Configuring copy command for linux host..." | |
# enable verbose mode if the trace flag is present | |
if ($Trace.IsPresent) { | |
$command += ( @" | |
set -v | |
"@ ) | |
} | |
# remove all the keys with the same comment | |
if ($UpdateMethod -eq 'replace') { | |
$command += ( @" | |
if [[ -f ~/.ssh/authorized_keys ]]; then | |
grep '$KeyComment' ~/.ssh/authorized_keys >> ~/.ssh/authorized_keys.removed | |
grep -v '$KeyComment' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.new | |
cat ~/.ssh/authorized_keys.new > ~/.ssh/authorized_keys | |
fi | |
"@ ) | |
} | |
# append the key to the authorized_keys file | |
$command += ( @" | |
cat <<EOF >> ~/.ssh/authorized_keys | |
$(Get-Content "$KeyFile.pub") | |
EOF | |
"@ ) | |
# -tt is needed to force the remote host to allocate a tty for the command (needed for cat <<EOF) | |
$options += @('-tt') | |
} | |
if ( $RemoteOS -eq 'windows' -and $supportedPowerShell ) { | |
Write-Host "Configuring copy command for windows host..." | |
# INFO: https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement#administrative-user | |
if ($AdminUser.IsPresent) { | |
$remoteKeyFile = "`${env:ProgramData}\ssh\administrators_authorized_keys" | |
} else { | |
$remoteKeyFile = "`${env:USERPROFILE}\.ssh\authorized_keys" | |
} | |
# enable verbose mode if the trace flag is present | |
if ($Trace.IsPresent) { | |
$command += ( @" | |
Set-PSDebug -Trace 1; | |
"@ ) | |
} | |
$command += ( @" | |
`$authorizedKeys = "$remoteKeyFile"; | |
"@ ) | |
# remove all the keys with the same comment if the update method is replace | |
if ($UpdateMethod -eq 'replace') { | |
$command += ( @" | |
if (Test-Path "`$authorizedKeys") { | |
`$content = (Get-Content -Raw "`$authorizedKeys"); | |
`$content | where { `$_ -match '$KeyComment' } | Out-File -Encoding utf8 -NoNewLine -Append -FilePath "`${authorizedKeys}.removed"; | |
`$content | where { `$_ -notmatch '$KeyComment' } | Out-File -Encoding utf8 -NoNewLine -FilePath "`${authorizedKeys}"; | |
} ; | |
"@ ) | |
} | |
$command += ( @" | |
'$(Get-Content "$KeyFile.pub")' | Out-File -Encoding utf8 -Append -FilePath "`${authorizedKeys}" | |
"@ ) | |
# we need to "escape" the double quotes and remove the new lines | |
$command = $command -replace '"','""' | |
$command = $command -replace "`n",' ' | |
# embed the command in a powershell command | |
$command = ( @" | |
powershell -Command "$command" | |
"@ ) | |
} | |
if ( $RemoteOS -eq 'windows' -and -not $supportedPowerShell ) { | |
Write-Host "Configuring copy command for windows host..." | |
# INFO: https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement#administrative-user | |
if ($AdminUser.IsPresent) { | |
$remoteKeyFile = "%ProgramData%\ssh\administrators_authorized_keys" | |
} else { | |
$remoteKeyFile = "%USERPROFILE%\.ssh\authorized_keys" | |
} | |
$command = "echo $(Get-Content "$KeyFile.pub") >> $remoteKeyFile" | |
} | |
if ($Trace.IsPresent) { | |
Write-Host | |
Write-Host "The following command will be executed:" | |
Write-Host "---------------------------------------------------" | |
Write-Host $command | |
Write-Host "---------------------------------------------------" | |
Write-Host | |
} | |
Write-Host | |
Write-Host "Connecting to the remote host..." | |
Write-Host | |
ssh $options $Alias $command | |
Write-Host | |
} | |
# show the key file location | |
Write-Host | |
Write-Host "The key file is located at: $KeyFile" | |
Write-Host | |
# show a command example to connect to the remote host | |
Write-Host "Use the following command to connect to the remote host:" | |
Write-Host "> ssh $Alias" | |
Write-Host | |
} | |
# workaround for powershell 7.2 or lower | |
function _IF { param( $P1, $P2, $P3 ); if ($P1) { $P2; } else { $P3; } } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment