fix(cli): redact interpolated secret values on the deploy accessory verb (#3159 leftover)#3206
Conversation
The #3159 fix wired $registerSecretsForRedaction() into DeployAppCli and DeployMainCli, but the standalone `wheels deploy accessory boot|reboot <name>` verbs route through DeployAccessoryCli, which shares no base class and never registered the resolver's secret values on its SshPool. AccessoryCommands embeds env.clear values — including ${SECRET}-interpolated ones — as `docker run ... -e 'KEY=value'` in argv, so a nonzero accessory exit surfaced the raw secret in the Wheels.Deploy.RemoteExecutionFailed message and CI logs: the exact leak #3159 closes, just on the accessory verb. DeployAccessoryCli now calls $registerSecretsForRedaction() in $forEach right after load() (ahead of any $dispatch), reading loader.secretResolver().all() the same way the other two CLIs do. A regression spec drives the leak path end-to-end: an env.clear value interpolated from a ${DB_ROOT_PW} token, a failed accessory docker run, and asserts the value is scrubbed to [REDACTED]. Refs #3159 Signed-off-by: Peter Amiri <petera@pai.com>
|
Superseded by #3205 (merged): the deferred-from-#3008 redaction of interpolated secret values in $raiseRemoteFailure summaries shipped there (redaction before the 200-char trim, SshClient/FakeSshPool mirror in lockstep), closing #3159. This propose-fix draft raced that campaign PR; #3205 landed first. Closing the duplicate. |
|
Correction: reopened — this is not superseded by #3205. #3205 wired secret-redaction into |
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR — This PR closes the last #3159 leak: the standalone wheels deploy accessory boot|reboot <name> verbs route through DeployAccessoryCli, which shares no base class with DeployAppCli/DeployMainCli and never registered the resolver's secret values on its SshPool. env.clear values interpolated from ${SECRET} tokens ride docker run ... -e KEY=value in argv (AccessoryCommands.$envArgs), so a nonzero exit surfaced the raw secret in Wheels.Deploy.RemoteExecutionFailed. The fix wires $registerSecretsForRedaction() into $forEach and adds an end-to-end regression spec. Verdict: approve — clean, well-scoped, faithfully mirrors established prior art.
Correctness — $registerSecretsForRedaction() is called in $forEach at DeployAccessoryCli.cfc:61 right after load() and ahead of every $dispatch/$deliverEnvFile. Placement is correct: $resolvedSecrets() reads variables.loader.secretResolver(), which load() populates — so the registration sees the live config (including all targets when name is "all", since the resolver is built for the whole config). The guard (structKeyExists(variables.sshPool, "$setSecretValues")) keeps it safe for pools that predate the method.
Cross-engine — No concerns. The new code is plain struct iteration (for (var k in resolved)), toString(), arrayAppend, structKeyExists — no obj.map(), no closure-as-constructor-arg, no reserved-scope shadowing. The test literal that assembles ${DB_ROOT_PW} correctly emits it as text while sidestepping CFML pound-sign interpolation.
Tests — The regression spec (DeployAccessoryCliSpec.cfc:152) drives the real leak path: it captures the exact docker run command via dry-run, asserts the raw value super-secret-acc-pw-9000 is present pre-fix, then replays it through a FakeSshPool returning exitCode 125 and asserts the thrown Wheels.Deploy.RemoteExecutionFailed message is scrubbed to [REDACTED]. FakeSshPool.$raiseRemoteFailure/$setSecretValues are byte-mirrors of SshClient, so this genuinely exercises the production scrub path rather than a stub.
Commits — fix(cli): redact interpolated secret values on the deploy accessory verb — valid type/scope, header under 100 chars, body explains the why, DCO sign-off present (Signed-off-by: Peter Amiri). Conforms to commitlint.config.js.
Docs — No new changelog fragment, which is correct here: changelog.d/3159-deploy-redact-remote-failure-secrets.security.md already documents the user-facing guarantee (resolved secret values are now redacted from RemoteExecutionFailed command summaries) generically across deploy verbs. This PR delivers on that existing promise for the accessory verb rather than introducing new user-facing behavior, so the fragment stays accurate as-is.
Nice work — the byte-for-byte mirror of the two sibling CLIs keeps the three dispatch paths consistent, and the test pins the regression at the argv boundary where it actually leaked.
Summary
Follow-up to #3205 (merged). That PR wired
$registerSecretsForRedaction()intoDeployAppCliandDeployMainCli, but the standalonewheels deploy accessory boot|reboot <name>verbs route throughDeployAccessoryCli, which shares no base class with the other two and never registered the resolver's secret values on itsSshPool.AccessoryCommands.$envArgsembedsenv.clearvalues — including${SECRET}-interpolated ones (e.g.MYSQL_ROOT_PASSWORD: '${DB_ROOT_PW}') — asdocker run ... -e 'KEY=value'in argv. So a nonzero accessory exit surfaced the raw secret in theWheels.Deploy.RemoteExecutionFailedmessage and CI logs: the exact leak #3159 closes, just on the accessory verb instead of the app verb.(
wheels deploy setup's accessory phase was already safe — it dispatches viaDeployMainCli.$dispatch, which does register. Only the standaloneaccessory boot/rebootverbs leaked.)Fix
DeployAccessoryCli.$forEachnow calls$registerSecretsForRedaction()right afterload()(ahead of any$dispatch), readingloader.secretResolver().all()the same wayDeployAppCli/DeployMainClido. The helper is a byte-for-byte mirror of the other two CLIs (guarded for pools that predate$setSecretValues).Test
A regression spec drives the leak path end-to-end: a postgres accessory with an
env.clearvalue interpolated from a${DB_ROOT_PW}token, a faileddocker run, asserting the value is scrubbed to[REDACTED]in the thrown message.CLI suite (lucee7 / sqlite): 1080 pass, 0 fail, 2 pre-existing/environmental errors (
SshClientSpec/SshPoolSpec—docker: command not foundfrom the docker-in-docker sshd fixture).Refs #3159