Skip to content

fix(cli): redact interpolated secret values on the deploy accessory verb (#3159 leftover)#3206

Merged
bpamiri merged 1 commit into
developfrom
fix/3159-accessory-verb-redact-leftover
Jun 13, 2026
Merged

fix(cli): redact interpolated secret values on the deploy accessory verb (#3159 leftover)#3206
bpamiri merged 1 commit into
developfrom
fix/3159-accessory-verb-redact-leftover

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #3205 (merged). That PR wired $registerSecretsForRedaction() into DeployAppCli and DeployMainCli, but the standalone wheels deploy accessory boot|reboot <name> verbs route through DeployAccessoryCli, which shares no base class with the other two and never registered the resolver's secret values on its SshPool.

AccessoryCommands.$envArgs embeds env.clear values — including ${SECRET}-interpolated ones (e.g. MYSQL_ROOT_PASSWORD: '${DB_ROOT_PW}') — 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 instead of the app verb.

(wheels deploy setup's accessory phase was already safe — it dispatches via DeployMainCli.$dispatch, which does register. Only the standalone accessory boot/reboot verbs leaked.)

Fix

DeployAccessoryCli.$forEach now calls $registerSecretsForRedaction() right after load() (ahead of any $dispatch), reading loader.secretResolver().all() the same way DeployAppCli/DeployMainCli do. 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.clear value interpolated from a ${DB_ROOT_PW} token, a failed docker 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/SshPoolSpecdocker: command not found from the docker-in-docker sshd fixture).

Refs #3159

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>
@github-actions github-actions Bot added the bug label Jun 13, 2026
@bpamiri

bpamiri commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@bpamiri bpamiri closed this Jun 13, 2026
@bpamiri bpamiri reopened this Jun 13, 2026
@bpamiri

bpamiri commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

Correction: reopened — this is not superseded by #3205. #3205 wired secret-redaction into DeployAppCli + DeployMainCli; this PR closes the third dispatch site, DeployAccessoryCli (wheels deploy accessory boot), which #3205 left uncovered — env.clear values interpolated from ${SECRET} tokens still ride docker run -e KEY=value on that path and would leak into a RemoteExecutionFailed summary / CI logs. Apologies for the premature close; it's a legitimate leftover-gap fix for #3159.

@bpamiri bpamiri enabled auto-merge (squash) June 13, 2026 18:39
@bpamiri bpamiri merged commit 59d6207 into develop Jun 13, 2026
13 of 14 checks passed
@bpamiri bpamiri deleted the fix/3159-accessory-verb-redact-leftover branch June 13, 2026 18:41

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Commitsfix(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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant