Module / version: github.com/k0sproject/rig/v2 v2.0.0 (latest v2 tag)
Related: #346 / #347 / #349 / #325 — those fixed many Windows-default-shell cases, but a general path remains.
Discovered while giving rig v2 a try (via the k0sctl dev branch) ahead of its tagged release.
Summary
powershell.Cmd() selects -EncodedCommand only when the script contains one of \n \r " % ! ^ & | < >. Any "simple" one-liner without those characters is instead emitted as an un-encoded -Command "$ProgressPreference='SilentlyContinue'; <cmd>". When the remote host's OpenSSH DefaultShell is powershell.exe, that outer login PowerShell parses and expands the $ tokens before the inner powershell.exe ever runs — expanding $ProgressPreference (an automatic variable, default value Continue) and any $env:… in the payload — so the command arrives corrupted.
Root cause
powershell/powershell.go:
func Cmd(psCmd string) string {
if strings.ContainsAny(psCmd, "\n\r\"%!^&|<>") {
return "... -NoP -E " + EncodeCmd(psCmd) // safe (base64)
}
return "... -NoP -Command \"" + withProgressPreference(psCmd) + "\"" // unsafe on a PowerShell login shell
}
withProgressPreference prepends $ProgressPreference='SilentlyContinue'; , which is exactly what the outer shell mangles. The -EncodedCommand branch is immune because base64 is opaque to the outer shell — which is why only the "simple" path reproduces this.
Symptom
Continue=SilentlyContinue : The term 'Continue=SilentlyContinue' is not recognized
as the name of a cmdlet, function, script file, or operable program.
and a non-zero exit. For a payload like $env:PROCESSOR_ARCHITECTURE the executed line becomes Continue='SilentlyContinue'; AMD64.
Reproduction
On a Windows host with OpenSSH DefaultShell = powershell.exe:
- Connect a
rig.Client over SSH.
- Run
client.ExecOutput("$env:PROCESSOR_ARCHITECTURE", cmd.PS()) (or any powershell.Cmd(<simple one-liner>)).
- Observe the
Continue=SilentlyContinue … not recognized error / non-zero exit.
It also surfaces through k0sctl (the dev branch, which is on rig v2) during apply against such a host — both are simple one-liners routed via ps.Cmd():
Host.Arch() runs $env:PROCESSOR_ARCHITECTURE → failed to detect host architecture: architecture not detected.
- the Windows configurer's Containers-feature check runs
(Get-WindowsFeature -Name Containers -ErrorAction SilentlyContinue).InstallState → failed to detect Containers feature state.
Why #346's fixes don't cover this
#347/#349 replaced cmd.exe-syntax commands (del /f, sc start, …) with cmd.PS()-wrapped PowerShell cmdlets. But cmd.PS() → Cmd() still routes simple payloads through the un-encoded -Command form, so any simple $-containing one-liner remains exposed on a PowerShell default shell.
Suggested fix
Make Cmd() robust regardless of the remote default shell:
- Always use
-EncodedCommand (EncodeCmd already exists; the only cost is less-readable command strings in trace logs, which can be mitigated by logging the decoded form). ~1-line change.
- Or keep
-Command only when the runner knows the remote default shell is cmd.exe, else encode.
- Or expose an opt-in (e.g.
WithForceEncodedPowerShell()) so consumers can force encoding on Windows.
Module / version:
github.com/k0sproject/rig/v2 v2.0.0(latest v2 tag)Related: #346 / #347 / #349 / #325 — those fixed many Windows-default-shell cases, but a general path remains.
Discovered while giving rig v2 a try (via the k0sctl
devbranch) ahead of its tagged release.Summary
powershell.Cmd()selects-EncodedCommandonly when the script contains one of\n \r " % ! ^ & | < >. Any "simple" one-liner without those characters is instead emitted as an un-encoded-Command "$ProgressPreference='SilentlyContinue'; <cmd>". When the remote host's OpenSSHDefaultShellispowershell.exe, that outer login PowerShell parses and expands the$tokens before the innerpowershell.exeever runs — expanding$ProgressPreference(an automatic variable, default valueContinue) and any$env:…in the payload — so the command arrives corrupted.Root cause
powershell/powershell.go:withProgressPreferenceprepends$ProgressPreference='SilentlyContinue';, which is exactly what the outer shell mangles. The-EncodedCommandbranch is immune because base64 is opaque to the outer shell — which is why only the "simple" path reproduces this.Symptom
and a non-zero exit. For a payload like
$env:PROCESSOR_ARCHITECTUREthe executed line becomesContinue='SilentlyContinue'; AMD64.Reproduction
On a Windows host with OpenSSH
DefaultShell = powershell.exe:rig.Clientover SSH.client.ExecOutput("$env:PROCESSOR_ARCHITECTURE", cmd.PS())(or anypowershell.Cmd(<simple one-liner>)).Continue=SilentlyContinue … not recognizederror / non-zero exit.It also surfaces through k0sctl (the
devbranch, which is on rig v2) duringapplyagainst such a host — both are simple one-liners routed viaps.Cmd():Host.Arch()runs$env:PROCESSOR_ARCHITECTURE→failed to detect host architecture: architecture not detected.(Get-WindowsFeature -Name Containers -ErrorAction SilentlyContinue).InstallState→failed to detect Containers feature state.Why #346's fixes don't cover this
#347/#349 replaced cmd.exe-syntax commands (
del /f,sc start, …) withcmd.PS()-wrapped PowerShell cmdlets. Butcmd.PS()→Cmd()still routes simple payloads through the un-encoded-Commandform, so any simple$-containing one-liner remains exposed on a PowerShell default shell.Suggested fix
Make
Cmd()robust regardless of the remote default shell:-EncodedCommand(EncodeCmdalready exists; the only cost is less-readable command strings in trace logs, which can be mitigated by logging the decoded form). ~1-line change.-Commandonly when the runner knows the remote default shell iscmd.exe, else encode.WithForceEncodedPowerShell()) so consumers can force encoding on Windows.