Skip to content

review remediation: deploy CLI remote-execution security — secret delivery, shell escaping, registry password redaction #2956

Description

@bpamiri

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 passwordSshClient.$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

  • No resolved secret appears in dry-run output, exception messages, or remote argv for registry login.
  • Every interpolated value reaching a remote/local shell is escaped; a config value containing a space or $( ) is handled safely.
  • Service/role names are format-validated before interpolation.
  • env.secret is either delivered to containers via env-file or rejected with a clear error.
  • Deploy CLI specs cover the escaping and redaction paths.

Source

Internal multi-agent framework review 2026-06-09, wave 2 (issues phase). Findings: followups DEP-2, DEP-3, DEP-4.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions