Problem
apppack config get and apppack config list print secret values in plaintext with no guardrails.
cmd/config.go:56-62 — config get calls GetParameter with WithDecryption: true and fmt.Printlns the raw value.
cmd/config.go:141 — config list dumps every variable and its decrypted value via ConfigVariables.ToConsole.
Config vars are SecureString params in SSM, so this includes SECRET_KEY, DATABASE_URL, API tokens, etc. In practice this means credentials end up in:
- shell scrollback and tmux/terminal history
- CI job logs when someone runs
config list in a pipeline
- screen shares and pairing sessions
- terminal recordings / support screenshots
There is no masking anywhere in the config path today (app/config.go has no redaction logic).
Proposal
Best-effort obfuscation of values that look like secrets, on by default for interactive output only.
1. Heuristic detection by variable name. Case-insensitive substring match on a deny-list, roughly:
SECRET, PASSWORD, PASSWD, TOKEN, API_KEY, APIKEY, PRIVATE_KEY,
CREDENTIAL, AUTH, SALT, SIGNATURE, ACCESS_KEY, SESSION_KEY, DSN
Plus values that parse as a URL with a non-empty userinfo section (catches DATABASE_URL, REDIS_URL, postgres://user:pw@host), where we mask only the password component rather than the whole value — the host/db name is useful and not sensitive.
2. Mask format. Show enough to identify without leaking: first 2 and last 2 chars for values > 8 chars, e.g. sk•••••••••••••4f, and a fixed •••••••• for short values. Keep it obvious that it's masked, not the literal value.
3. Escape hatches. Masking must never break automation:
- Skip masking entirely when stdout is not a TTY (
config get FOO | ..., $(apppack config get FOO) in scripts). config list already checks isatty at cmd/config.go:136, so the plumbing exists.
--reveal (or --no-mask) flag on get and list to print plaintext in a TTY.
config export stays unmasked always — it exists to produce a machine-readable dump and is the documented backup/restore path. Same for --json.
4. Print a hint when something was masked: 1 value masked — use --reveal to show so nobody thinks the value is literally bullets.
Notes / open questions
- This is deliberately a heuristic. It will miss things (
STRIPE_SK, FOO_PW) and over-mask others (AUTH_ENABLED=true). That's the accepted tradeoff — the goal is reducing accidental exposure, not a security boundary. Worth saying so in the docs.
- Alternative to a name deny-list: mask everything by default in TTY mode and require
--reveal to see any value. Safer and much simpler to reason about, but more annoying day-to-day. Leaning toward the heuristic, open to being talked into blanket masking for config list.
- Should
--reveal be gated behind a confirmation prompt? Probably not — too noisy.
Scope
apppack/app/config.go — masking helper + tests (table-driven over name/value pairs)
apppack/cmd/config.go — wire into get and list, add --reveal
apppack-docs — note the behavior and the escape hatch in the config command reference
Problem
apppack config getandapppack config listprint secret values in plaintext with no guardrails.cmd/config.go:56-62—config getcallsGetParameterwithWithDecryption: trueandfmt.Printlns the raw value.cmd/config.go:141—config listdumps every variable and its decrypted value viaConfigVariables.ToConsole.Config vars are SecureString params in SSM, so this includes
SECRET_KEY,DATABASE_URL, API tokens, etc. In practice this means credentials end up in:config listin a pipelineThere is no masking anywhere in the config path today (
app/config.gohas no redaction logic).Proposal
Best-effort obfuscation of values that look like secrets, on by default for interactive output only.
1. Heuristic detection by variable name. Case-insensitive substring match on a deny-list, roughly:
Plus values that parse as a URL with a non-empty userinfo section (catches
DATABASE_URL,REDIS_URL,postgres://user:pw@host), where we mask only the password component rather than the whole value — the host/db name is useful and not sensitive.2. Mask format. Show enough to identify without leaking: first 2 and last 2 chars for values > 8 chars, e.g.
sk•••••••••••••4f, and a fixed••••••••for short values. Keep it obvious that it's masked, not the literal value.3. Escape hatches. Masking must never break automation:
config get FOO | ...,$(apppack config get FOO)in scripts).config listalready checksisattyatcmd/config.go:136, so the plumbing exists.--reveal(or--no-mask) flag ongetandlistto print plaintext in a TTY.config exportstays unmasked always — it exists to produce a machine-readable dump and is the documented backup/restore path. Same for--json.4. Print a hint when something was masked:
1 value masked — use --reveal to showso nobody thinks the value is literally bullets.Notes / open questions
STRIPE_SK,FOO_PW) and over-mask others (AUTH_ENABLED=true). That's the accepted tradeoff — the goal is reducing accidental exposure, not a security boundary. Worth saying so in the docs.--revealto see any value. Safer and much simpler to reason about, but more annoying day-to-day. Leaning toward the heuristic, open to being talked into blanket masking forconfig list.--revealbe gated behind a confirmation prompt? Probably not — too noisy.Scope
apppack/app/config.go— masking helper + tests (table-driven over name/value pairs)apppack/cmd/config.go— wire intogetandlist, add--revealapppack-docs— note the behavior and the escape hatch in the config command reference