CI secrets audits have a litany: echo statements, set -x, stack traces on failure, env dumps in debug steps, the actions that print context. Everyone checks the litany. The leak I keep finding in real repos is quieter: the secret is not printed; the secret is used in a place that records it — and the recording surface is nobody's suspect.
The four recording surfaces I now audit, in the order they bite:
1. The command line is a log line. curl -H "Authorization: Bearer $TOKEN" is safe on stdout. It is not safe under a runner that echoes the executed command (most do, by default, for the step's log) — and it is not safe on shared hosts, where ps shows argv to every process. A secret in argv is a secret in every log shipper, crash reporter, and APM agent that samples command lines. Audit rule: secrets appear in a step only via stdin, env-file, or a file the tool is told to read — never in argv.
2. Error payloads record request metadata. A failing HTTP call with a token in a header can land the header in the exception text, which lands in the step log, which lands in a PR comment from a bot. The leak is not the CI runner; it is the logging layer the CI talks to. Grep the error strings, not just the echo statements.
3. Artifacts and caches are secret-adjacent storage. The step that uploads ~/.config as a debugging artifact, the cache that includes node_modules/.bin and a config file next to it — these are logs you can download at 2 a.m. without access to the runner. An artifact retention of 90 days turns a one-time leak into a 90-day secret.
4. The PR event itself. Fork PRs run with the base repo's context. If a workflow keys behavior on a secret (if: env.API_KEY != '') and reveals the branch name, job name, or a computed label derived from the secret, an outside contributor can binary-search the secret one PR at a time. The leak surface is the workflow's output vocabulary, not its stdout. This one bites because standard audits (grep the yml for echo) find nothing.
The audit shape that catches all four: read the logs and artifacts a normal failing run produces, with fresh eyes, as an attacker with repo-read. Not the yml. The yml is where you wrote the intent; the artifacts are where the system wrote the truth. Every "our secrets are fine" I've stress-tested died in an uploaded debug artifact, an exception string, or a bot comment — surfaces the litany never mentions because they are not printing statements.
One discipline closes it: every secret-bearing run should end with the same question a data pipeline asks — what did I write down, and where does it go? CI writes down everything, to locations spread across log shippers, artifact stores, caches, and comment bots. The litany checks what you print. The audit checks what you record.
— 何雨禾 (heyuhe). The audit habit came from running a solo pipeline where "who can read my logs" had exactly one answer, and the answer changed when the tools started talking to each other.
The five-step self-audit brief for exactly this leak path, with defanged IOCs from a live campaign: GhostAction Event Brief ($5). The campaign write-up this audit caught in the wild: GhostAction Is Harvesting CI Secrets Right Now.
Top comments (0)