Skip to content

feat(proxy): redact basic-auth and move the cache store off the command line (#93 workstream C) - #95

Merged
mhenrixon merged 1 commit into
dashfrom
feat/proxy-secret-redaction
Aug 3, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
feat/proxy-secret-redaction

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

Workstream C of the v3.0.0 release gate (#93) — secrets & redaction. Builds on B's LB run surface (#94).

  • C1 — basic-auth redaction. Proxy#basic_auth_credential returns a Kamal::Utils.sensitive value (registry-login precedent), and Utils.optionize gains a Sensitive passthrough: the rendered --basic-auth="admin:s3cr3t" keeps the real value for execution and prints as --basic-auth=[REDACTED] everywhere SSHKit logs — at :info verbosity it used to land in plain text.
  • C2 — cache store off the command line. kamal-proxy reads CACHE_STORE from its environment as the --cache-store default (run.go:50, verified against the published image), so the URL now travels in the proxy secrets env file — acme.env generalized to .kamal/proxy/secrets.env (0600), carrying ACME credentials and the store together, uploaded to proxy hosts and the LB host alike (the LB owns the cache after feat(proxy): explicit loadbalancer layering contract + LB run plumbing (#93 workstream B) #94). The drift digest hashes secret names, never values: adding/removing the store reboots the proxy; rotating the URL's value needs an explicit kamal proxy reboot — same contract as ACME credentials, now documented. The docs caveat that documented the leak is gone; --cache-store joins the flag-coverage waivers with the reason.
  • C3 — cleanup. Boot and reboot (proxy hosts and the LB host) remove .kamal/proxy/secrets.env when the config no longer needs one — a host keeps no secrets it no longer needs.

Refs #93 (workstream C — D–G continue in follow-up PRs)

Test plan

  • Raw argv keeps the credential; Kamal::Utils.redacted form never contains it (app, loadbalancer, configuration, accessory levels)
  • Store lands in secrets.env and never in run_command, docker options, printed boot output, or digest inputs; acme + store share one env file
  • Digest moves on store presence, not value (memory ↔ redis URL is digest-identical)
  • CLI boot uploads CACHE_STORE 0600, never prints the URL; stale secrets.env removed when config gone
  • bundle exec rubocop --parallel clean; unit suite green (builder failures are the known Apple-Silicon artifacts)
  • CI (incl. integration on amd64)

Deviations & judgment calls

  • Judgment call: the whole credential redacts as --basic-auth=[REDACTED] — username included, matching the registry-login precedent (-u [REDACTED]), rather than admin:[REDACTED].
  • Judgment call: optionize passes through caller-marked Sensitive values per-value instead of growing a blanket sensitive: kwarg like argumentize — only one of a command's options is secret here, and per-value marking keeps the rest of the argv readable in logs.
  • Deviation: renamed acme.env → secrets.env (free — nothing since 2.12.0 is released). A host booted from a pre-release dash build with ACME keeps a stale acme.env; harmless (nothing references it, kamal proxy remove deletes the directory), so no migration code.
  • Judgment call: digest follows the settled presence/name rule literally, so store: memory → store: redis://… does not auto-reboot — the operator runs kamal proxy reboot, as documented. If you'd rather drift on a credential-stripped form of the URL (scheme+host, no userinfo), it's a two-line change to secret_names.
  • Judgment call: CACHE_STORE goes through the env file even when the value is the non-secret memory — uniform delivery beats a special case, and kamal-proxy treats the env var identically.
  • Note: C3's removal is an unconditional rm (with raise_on_non_zero_exit: false) on every boot/reboot when no secrets are configured — one extra no-op exec per host, mirroring the upload branches as v3.0.0 release gate: LB layering discipline, secret redaction, surface freeze, port_holder, docs tiering #93 specified.

…nd line

## Summary

Workstream C of the v3.0.0 release gate (#93): nothing knowingly leaky ships
under a 3.0 banner.

C1: the basic-auth credential is wrapped in Kamal::Utils.sensitive (registry-
login precedent), and Utils.optionize gains a Sensitive passthrough so the
rendered --basic-auth option keeps the real value for execution and a
[REDACTED] form for anything kamal prints - at :info verbosity it used to
land in plain text.

C2: the cache store URL never touches the docker run command line. kamal-proxy
reads CACHE_STORE from its environment as the --cache-store default, so the
URL travels in the proxy secrets env file - the acme.env mechanism generalized
to .kamal/proxy/secrets.env (0600), carrying ACME credentials and the store
together, on proxy hosts and the loadbalancer host alike. The drift digest
hashes the secret *names*, never values: adding/removing the store reboots
the proxy, rotating the URL needs an explicit `kamal proxy reboot` (same
contract as acme credentials, now documented; the docs caveat that documented
the leak is gone). --cache-store joins the flag-coverage waivers with the
reason.

C3: a host keeps no secrets it no longer needs - boot and reboot (proxy hosts
and the LB host) remove .kamal/proxy/secrets.env when the config no longer
calls for one.

## Test Coverage

- raw argv keeps the credential, redacted form never contains it (app,
  loadbalancer, configuration levels)
- store lands in secrets.env and never in run_command, docker options,
  printed boot output, or digest inputs; acme + store share one env file
- digest moves on store presence, not value
- CLI boot uploads CACHE_STORE 0600 and removes a stale secrets.env

## Verification

- [x] bundle exec rubocop --parallel passes
- [x] unit suite passes (builder failures are the known host-arch artifacts)

Refs #93
@mhenrixon mhenrixon self-assigned this Aug 3, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Aug 3, 2026
@mhenrixon
mhenrixon merged commit de2a11f into dash Aug 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant