Skip to content

Instantly share code, notes, and snippets.

@KaiWalter
Last active May 6, 2024 02:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KaiWalter/b5dc222b1ff67f618b9ff076ca3d6a21 to your computer and use it in GitHub Desktop.
Save KaiWalter/b5dc222b1ff67f618b9ff076ca3d6a21 to your computer and use it in GitHub Desktop.
Manage SSH config file entries with PowerShell
# 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
Copy link

@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

@KaiWalter
Copy link
Author

@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

@sdoffen
Copy link

sdoffen commented May 16, 2023

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.

@KaiWalter
Copy link
Author

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