Last active
May 6, 2024 02:49
-
-
Save KaiWalter/b5dc222b1ff67f618b9ff076ca3d6a21 to your computer and use it in GitHub Desktop.
Manage SSH config file entries with PowerShell
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
# samples | |
# $HostList = Get-ConfigHostList | |
# $HostList = Add-ConfigHostInList -HostList $HostList -HostName "dummy" -HostValues @{ | |
# identityfile = "~/.ssh/myprivatekey" | |
# hostname = "dummy.somecloud.com" | |
# user = "johndoe" | |
# } | |
# $HostList = Update-ConfigHostInList -HostList $HostList -HostName "dummy" -HostValues @{ | |
# identityfile = "~/.ssh/myprivatekey" | |
# hostname = "dummy.somecloud.com" | |
# user = "johndoe" | |
# } | |
# $HostList = Remove-ConfigHostFromList -HostList $HostList -HostName "dummy" | |
# Set-ConfigHostList $HostList | |
# Get-ConfigHostList | Remove-ConfigHostFromList -HostName "dummy" | Set-ConfigHostList | |
function Get-LineBreaks { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string] $Contents | |
) | |
# determine line break LF or CR/LF | |
if ($Contents -match "^[^\n]+\r\n") { | |
$splitter = "\r\n" | |
$joiner = "`r`n" | |
} | |
else { | |
$splitter = "\n" | |
$joiner = "`n" | |
} | |
return $splitter, $joiner | |
} | |
function Get-ConfigFileName { | |
return Join-Path $HOME ".ssh" "config" -Resolve | |
} | |
function Get-ConfigContents { | |
$configFilename = Get-ConfigFileName | |
return Get-Content $configFilename -Raw | |
} | |
function Set-ConfigContents { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string] $Contents | |
) | |
$configFilename = Get-ConfigFileName | |
if ($Contents) { | |
$Contents | Set-Content $configFilename | |
} | |
} | |
function Get-ConfigKeyWords { | |
return @("Match", | |
"AddressFamily", | |
"BatchMode", | |
"BindAddress", | |
"ChallengeResponseAuthentication", | |
"CheckHostIP", | |
"Cipher", | |
"Ciphers", | |
"ClearAllForwardings", | |
"Compression", | |
"CompressionLevel", | |
"ConnectionAttempts", | |
"ConnectTimeout", | |
"ControlMaster", | |
"ControlPath", | |
"DynamicForward", | |
"EscapeChar", | |
"ExitOnForwardFailure", | |
"ForwardAgent", | |
"ForwardX11", | |
"ForwardX11Trusted", | |
"GatewayPorts", | |
"GlobalKnownHostsFile", | |
"GSSAPIAuthentication", | |
"GSSAPIKeyExchange", | |
"GSSAPIClientIdentity", | |
"GSSAPIDelegateCredentials", | |
"GSSAPIRenewalForcesRekey", | |
"GSSAPITrustDns", | |
"HashKnownHosts", | |
"HostbasedAuthentication", | |
"HostKeyAlgorithms", | |
"HostKeyAlias", | |
"HostName", | |
"IdentitiesOnly", | |
"IdentityFile", | |
"KbdInteractiveAuthentication", | |
"KbdInteractiveDevices", | |
"LocalCommand", | |
"LocalForward", | |
"LogLevel", | |
"MACs", | |
"NoHostAuthenticationForLocalhost", | |
"PreferredAuthentications", | |
"Protocol", | |
"ProxyCommand", | |
"PubkeyAuthentication", | |
"RemoteForward", | |
"RhostsRSAAuthentication", | |
"RSAAuthentication", | |
"SendEnv", | |
"ServerAliveCountMax", | |
"ServerAliveInterval", | |
"SmartcardDevice", | |
"StrictHostKeyChecking", | |
"TCPKeepAlive", | |
"Tunnel", | |
"TunnelDevice", | |
"UsePrivilegedPort", | |
"User", | |
"UserKnownHostsFile", | |
"VerifyHostKeyDNS", | |
"VisualHostKey") | |
} | |
function Get-ConfigHostList { | |
$keywords = Get-ConfigKeyWords | |
$contents = Get-ConfigContents | |
$splitter, $joiner = Get-LineBreaks $contents | |
# split by "Host" - when at start of file or has prededing line breaks / whitespaces | |
$splitEntries = "(?i)(^|" + $splitter + "+\s+)host\s" | |
$list = [regex]::Split($contents, $splitEntries) | |
if ($list.Count -le 1) { | |
throw "splitting file $configFilename failed or no content" | |
} | |
# READ lists of hosts | |
$HostList = @{} | |
foreach ($entry in $list) { | |
# $output += $entry -replace $($splitter+"\s+"), $($joiner+" ") | |
$attributes = [regex]::Split($entry, $splitter) | % { $_.Trim() } | |
$HostName = $null | |
$HostValues = @{} | |
foreach ($attribute in $attributes) { | |
if ($attribute -ne "") { | |
if ($HostName) { | |
# split key/value and normalize key name | |
$kv = [regex]::Split($attribute, "\s+", 1) | |
$keyName = $kv[0] | |
$keyValue = $kv[1] | |
foreach ($keyword in ($keywords | ? { $_ -eq $keyName })) { | |
$keyName = $keyword | |
break | |
} | |
$HostValues[$keyName] = $keyValue | |
} | |
else { | |
# assume first entry to be the host | |
$HostName = $attribute.ToLower() | |
} | |
} | |
} | |
if ($HostName) { | |
if ($HostList.ContainsKey($HostName)) { | |
throw "duplicate Host $HostName" | |
} | |
else { | |
$HostList[$HostName] = $HostValues | |
} | |
} | |
} | |
return $HostList | |
} | |
function Get-ConfigHostFromList { | |
param ( | |
[Parameter(Mandatory = $true, ValueFromPipeline=$true)] | |
[hashtable] | |
$HostList, | |
[Parameter(Mandatory = $true)] | |
[string] | |
$HostName, | |
[switch] | |
$IgnoreNonExisting | |
) | |
$hostEntry = $null | |
if ($HostList.ContainsKey($HostName.ToLower())) { | |
$hostEntry = $HostList[$HostName.ToLower()] | |
} | |
else { | |
if (!$IgnoreNonExisting) { | |
throw "HostName $HostName does not exist" | |
} | |
} | |
return $hostEntry | |
} | |
function Update-ConfigHostInList { | |
param ( | |
[Parameter(Mandatory = $true, ValueFromPipeline=$true)] | |
[hashtable] | |
$HostList, | |
[Parameter(Mandatory = $true)] | |
[string] | |
$HostName, | |
[Parameter(Mandatory = $true)] | |
[hashtable] | |
$HostValues | |
) | |
if ($HostList.ContainsKey($HostName.ToLower())) { | |
$HostList = Remove-ConfigHostFromList -HostList $HostList -HostName $HostName | |
$HostList = Add-ConfigHostToList -HostList $HostList -HostName $HostName -HostValues $HostValues | |
} | |
else { | |
throw "HostName $HostName does not exist" | |
} | |
return $HostList | |
} | |
function Set-ConfigHostList { | |
param ( | |
[Parameter(Mandatory = $true, ValueFromPipeline=$true)] | |
[hashtable] $HostList | |
) | |
$splitter, $joiner = Get-LineBreaks $(Get-ConfigContents) | |
$output = @() | |
foreach ($hostEntry in $HostList.GetEnumerator()) { | |
$hostOutput = "Host " + $hostEntry.Key + $joiner | |
foreach ($kv in $hostEntry.Value.GetEnumerator()) { | |
$hostOutput = $hostOutput + " " + $kv.key + " " + $kv.value + $joiner | |
} | |
$output += $hostOutput | |
} | |
if ($output) { | |
Set-ConfigContents $($output -join $($joiner)) | |
} | |
else { | |
throw "no entries in hostlist - will not overwrite" | |
} | |
} | |
function Add-ConfigHostToList { | |
param ( | |
[Parameter(Mandatory = $true, ValueFromPipeline=$true)] | |
[hashtable] | |
$HostList, | |
[Parameter(Mandatory = $true)] | |
[string] | |
$HostName, | |
[Parameter(Mandatory = $true)] | |
[hashtable] | |
$HostValues, | |
[switch] | |
$IgnoreExisting | |
) | |
if (!$IgnoreExisting) { | |
if ($HostList.ContainsKey($HostName.ToLower())) { | |
throw "HostName $HostName already exists" | |
} | |
} | |
$keywords = Get-ConfigKeyWords | |
$hostValuesCleaned = @{} | |
foreach ($kv in $HostValues.GetEnumerator()) { | |
$keyName = $null | |
foreach ($keyword in ($keywords | ? { $_ -eq $kv.Key })) { | |
$keyName = $keyword | |
break | |
} | |
if ($keyName) { | |
$hostValuesCleaned[$keyName] = $kv.Value.Trim() | |
} | |
else { | |
throw "key $($kv.Key) not found in list of keywords" | |
} | |
} | |
if ($HostValuesCleaned) { | |
$hostList[$HostName.ToLower()] = $HostValuesCleaned | |
} | |
return $HostList | |
} | |
function Remove-ConfigHostFromList { | |
param ( | |
[Parameter(Mandatory = $true, ValueFromPipeline=$true)] | |
[hashtable] | |
$HostList, | |
[Parameter(Mandatory = $true)] | |
[string] | |
$HostName | |
) | |
if ($HostList.ContainsKey($HostName.ToLower())) { | |
$HostList.Remove($HostName.ToLower()) | |
} | |
else { | |
throw "HostName $HostName not found in list" | |
} | |
return $HostList | |
} | |
Export-ModuleMember Get-ConfigHost* | |
Export-ModuleMember Set-ConfigHost* | |
Export-ModuleMember Add-ConfigHost* | |
Export-ModuleMember Remove-ConfigHost* | |
Export-ModuleMember Update-ConfigHost* |
@JannikZed my bad - this particular PowerShell Core syntax Join-Path $HOME ".ssh" "config" -Resolve
is not supported by Windows PowerShell - I managed to get the same result in WinPwSh with Join-Path $HOME $(Join-Path ".ssh" "config") -Resolve
My config file looks like this:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/personal
IdentityFile ~/.ssh/professional
The parser only allows for a single value for IdentityFile
, while the specification allows for multiple.
My config file looks like this:
Host * AddKeysToAgent yes UseKeychain yes IdentityFile ~/.ssh/personal IdentityFile ~/.ssh/professional
The parser only allows for a single value for
IdentityFile
, while the specification allows for multiple.
Thanks. I see. Was not aware of that.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@KaiWalter the function "Get-ConfigFileName" did not work for us:
Join-Path : Es wurde kein Positionsparameter gefunden, der das Argument "config" akzeptiert.
I had to change it to:
return Join-Path $HOME ".ssh/config" -Resolve