Skip to content

Instantly share code, notes, and snippets.

@Scherlac
Last active December 4, 2023 21:40
Show Gist options
  • Save Scherlac/a486301725b6ee18876c4f7a81cc6de9 to your computer and use it in GitHub Desktop.
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
<#
.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