Problem
The wheels deploy Kamal port has a remote-execution security architecture problem: secrets leak through multiple channels, no value is ever shell-escaped before reaching a remote shell, and env.secret is parsed but never delivered. These are one design problem — how commands and secrets reach the remote host. Paths below are relative to cli/lucli/services/deploy/; verified against origin/develop @ 8971094.
1. Registry password leaks through three channels (DEP-2, High)
commands/RegistryCommands.cfc:12-20 renders docker login <server> -u <user> -p <password> as a flat string. Leak paths: (1) dry-run prints it verbatim — $dispatch appends the full command to the dry-run buffer (cli/DeployRegistryCli.cfc:42, 53, 58-67, 79, 84-85), so wheels deploy registry login --dry-run prints the resolved secret; (2) failed login throws the password — SshClient.$raiseRemoteFailure embeds the first 200 chars of the command in the exception message (lib/SshClient.cfc:146-147, 179-186), so a wrong server/expired credential lands the password in CI logs; (3) remote argv exposure — -p <password> is visible in the remote process table, unescaped. Kamal uses --password-stdin + sensitive(..., redaction:) for exactly these reasons.
2. No shell escaping anywhere in remote command construction (DEP-3, High)
commands/Base.cfc:8-19 space-joins tokens; nothing in the deploy tree quotes values before they hit sess.exec() (lib/SshClient.cfc:118) or local bash -c (cli/DeployBuildCli.cfc:115). Concrete sites: env values as unquoted -e KEY=value (commands/AppCommands.cfc:96-104, commands/AccessoryCommands.cfc:90-98) — spaces break docker run, backticks/$()/; execute on the remote host. ConfigLoader.$interpolate expands ${SECRET} tokens from .kamal/secrets into config strings (config/ConfigLoader.cfc:92-116), so secrets routed into env.clear flow unescaped into remote argv and the DEP-2 exception summaries. Service/role names are never format-validated (config/Validator.cfc:31-41) yet interpolated raw into lockPath() (commands/LockCommands.cfc:25-27), label filters piped to xargs docker rm -f (cli/DeployMainCli.cfc:270-271, commands/PruneCommands.cfc:29-34), and container names. Local build context path is interpolated into [bash, -c, cmd] (commands/BuilderCommands.cfc:12-21). Same defect class as jobs-misc J8 (DatabaseShellHelper).
3. env.secret is parsed but never delivered to containers (DEP-4, High)
config/Env.cfc:22-26 exposes secret() but a repo-wide grep finds zero call sites — only clear() is consumed (commands/AppCommands.cfc:98, commands/AccessoryCommands.cfc:92). There is no env-file build / SFTP-upload / --env-file mechanism anywhere. A user who declares env: secret: [DATABASE_PASSWORD] per the Kamal docs ships containers without that variable, and the natural workaround — moving it to clear: — funnels the secret into the DEP-3 plaintext-argv path.
Impact
Anyone running wheels deploy against a real host risks leaking registry/DB credentials into CI logs, terminals, bug reports, and the remote process table; shell metacharacters in any config value break or inject into the remote shell; and the documented env.secret feature silently ships containers without the declared secret.
Suggested approach
Per campaign guidance, this needs a design pass on the Kamal-port command builder — do not autonomously patch secret handling:
- Switch registry login to
printf '%s' <escaped> | docker login --password-stdin; redact the password token in dry-run output and in $raiseRemoteFailure's command summary.
- Promote
SecretResolver.$shellEscape (lib/SecretResolver.cfc:116-118) into Base.cfc and apply it to every interpolated value; add a docker-compliant service-name format check to Validator.cfc.
- Implement env-file delivery (
SshClient already has uploadString, lib/SshClient.cfc:218-232) or hard-error on a non-empty env.secret until supported.
Acceptance criteria
Source
Internal multi-agent framework review 2026-06-09, wave 2 (issues phase). Findings: followups DEP-2, DEP-3, DEP-4.
Problem
The
wheels deployKamal port has a remote-execution security architecture problem: secrets leak through multiple channels, no value is ever shell-escaped before reaching a remote shell, andenv.secretis parsed but never delivered. These are one design problem — how commands and secrets reach the remote host. Paths below are relative tocli/lucli/services/deploy/; verified againstorigin/develop@8971094.1. Registry password leaks through three channels (DEP-2, High)
commands/RegistryCommands.cfc:12-20rendersdocker login <server> -u <user> -p <password>as a flat string. Leak paths: (1) dry-run prints it verbatim —$dispatchappends the full command to the dry-run buffer (cli/DeployRegistryCli.cfc:42, 53, 58-67, 79, 84-85), sowheels deploy registry login --dry-runprints the resolved secret; (2) failed login throws the password —SshClient.$raiseRemoteFailureembeds the first 200 chars of the command in the exception message (lib/SshClient.cfc:146-147, 179-186), so a wrong server/expired credential lands the password in CI logs; (3) remote argv exposure —-p <password>is visible in the remote process table, unescaped. Kamal uses--password-stdin+sensitive(..., redaction:)for exactly these reasons.2. No shell escaping anywhere in remote command construction (DEP-3, High)
commands/Base.cfc:8-19space-joins tokens; nothing in the deploy tree quotes values before they hitsess.exec()(lib/SshClient.cfc:118) or localbash -c(cli/DeployBuildCli.cfc:115). Concrete sites: env values as unquoted-e KEY=value(commands/AppCommands.cfc:96-104,commands/AccessoryCommands.cfc:90-98) — spaces breakdocker run, backticks/$()/;execute on the remote host.ConfigLoader.$interpolateexpands${SECRET}tokens from.kamal/secretsinto config strings (config/ConfigLoader.cfc:92-116), so secrets routed intoenv.clearflow unescaped into remote argv and the DEP-2 exception summaries. Service/role names are never format-validated (config/Validator.cfc:31-41) yet interpolated raw intolockPath()(commands/LockCommands.cfc:25-27), label filters piped toxargs docker rm -f(cli/DeployMainCli.cfc:270-271,commands/PruneCommands.cfc:29-34), and container names. Local build context path is interpolated into[bash, -c, cmd](commands/BuilderCommands.cfc:12-21). Same defect class as jobs-misc J8 (DatabaseShellHelper).3.
env.secretis parsed but never delivered to containers (DEP-4, High)config/Env.cfc:22-26exposessecret()but a repo-wide grep finds zero call sites — onlyclear()is consumed (commands/AppCommands.cfc:98,commands/AccessoryCommands.cfc:92). There is no env-file build / SFTP-upload /--env-filemechanism anywhere. A user who declaresenv: secret: [DATABASE_PASSWORD]per the Kamal docs ships containers without that variable, and the natural workaround — moving it toclear:— funnels the secret into the DEP-3 plaintext-argv path.Impact
Anyone running
wheels deployagainst a real host risks leaking registry/DB credentials into CI logs, terminals, bug reports, and the remote process table; shell metacharacters in any config value break or inject into the remote shell; and the documentedenv.secretfeature silently ships containers without the declared secret.Suggested approach
Per campaign guidance, this needs a design pass on the Kamal-port command builder — do not autonomously patch secret handling:
printf '%s' <escaped> | docker login --password-stdin; redact the password token in dry-run output and in$raiseRemoteFailure's command summary.SecretResolver.$shellEscape(lib/SecretResolver.cfc:116-118) intoBase.cfcand apply it to every interpolated value; add a docker-compliant service-name format check toValidator.cfc.SshClientalready hasuploadString,lib/SshClient.cfc:218-232) or hard-error on a non-emptyenv.secretuntil supported.Acceptance criteria
$( )is handled safely.env.secretis either delivered to containers via env-file or rejected with a clear error.Source
Internal multi-agent framework review 2026-06-09, wave 2 (issues phase). Findings: followups DEP-2, DEP-3, DEP-4.