I'm trying to send a PowerShell script as a string through SSH in PowerShell 5.1. Its objective is to modify the content of a configuration file in a remote computer. I managed to fix the problems relating to escaping the special character sequences thanks to this answer in my previous question. However, I assumed that ssh $Username@$ComputerIP $stringScript
would execute similarly to cmd.exe /c $stringScript
but from my testing it doesn't, they are processed differently at a level I don't understand, as the code with ssh gives an error.
Here's my intended code:
$forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
$rmtPSAuthOnlyKeys = "powershell Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
"-Value \`"$forceOnlyKeysSSH\`""
ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys
Ideally it would modify the sshd_config
file with
Match all
PasswordAuthentication no
But I'm getting this error, that to me suggest is dividing the arguments around the spaces.
Add-Content : A positional parameter cannot be found that accepts argument 'all
PasswordAuthentication'.
At line:1 char:1
+ Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config -Val ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Add-Content], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.AddContentCommand
Am I wrongly escaping the string? Why does this error not show up when calling cmd.exe /c $rmtPSAuthOnlyKeys
?
If you're calling ssh.exe
from Windows PowerShell or PowerShell 7 v7.2.x or below, you unfortunately need to compensate for a bug that affects arguments with embedded "
characters.
It so happens that the bug is canceled out when cmd.exe /c
is called directly, due to cmd.exe
's own parsing quirks.
All other external programs are affected, however, including ssh.exe
.
Therefore, with a default SSH server configuration on Windows, even though the command passed to the ssh.exe
client is ultimately passed to cmd /c
, the bug must be dealt with.
See the workaround below.
This bug is fixed in PowerShell (Core) 7 versions 7.3 and above, so no additional effort is required when calling from there.
See this answer for background information.
Workaround:
Specifically, in Windows PowerShell and PowerShell 7.2- you must manually \
-escape "
chars. (double quotes) that are embedded in arguments.
Here's a cross-edition solution that applies the manual escaping only when needed:
$forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
$rmtPSAuthOnlyKeys = "powershell -NoProfile Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
"-Value \`"$forceOnlyKeysSSH\`""
if ($PSVersionTable.PSVersion -lt '7.3') { # Workaround needed
# Manually add another round of \-escaping to the embedded, escaped \" chars.
$rmtPSAuthOnlyKeys = $rmtPSAuthOnlyKeys.Replace('\"', '\\\"')
}
ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys
Note:
As written, the value stored in $forceOnlyKeysSSH
is subject to whitespace normalization, so that, e.g.,
foo bar
would be passed as foo bar
; that is, runs of two or more spaces are collapsed into a single space each.
If that is a concern, more work is needed, as detailed in the answer to your previous question - note that the need for the additional manual \
-escaping equally applies situationally; to spell out the - more cumbersome - solution without whitespace normalization:
$forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
# Note the (escaped) "..." enclosure around the (implied) -Command argument
# and the (escaped) "^""..."^"" enclosure around $forceOnlyKeysSSH
$rmtPSAuthOnlyKeys = "powershell -NoProfile `"Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
"-Value `"^`"`"$forceOnlyKeysSSH`"^`"`"`""
if ($PSVersionTable.PSVersion -lt '7.3') { # Workaround needed
# Manually add another round of \-escaping to the embedded " chars.
$rmtPSAuthOnlyKeys = $rmtPSAuthOnlyKeys.Replace('"', '\"')
}
ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys