Git, CI/CD & GitOpsXLII · CI SecretsSecrets
Log leakage risks — failure modes that bypass the masker
What you'll learn
- Enumerate the side channels that bypass the log masker: artifacts, child processes, webhooks, third-party error responses
- Identify the script patterns that reliably leak secrets: debug flags, verbose modes, stack traces, command-line argument echoing
- Distinguish a leaked log from a leaked credential and explain why the distinction is operational, not semantic
- Apply the rotation-and-audit response to a log leak, and explain why rotation is the only correct response
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A leaked log is a leaked credential. The distinction between “the secret appeared in the log” and “the secret was disclosed to an attacker” is operational, not semantic: the secret is in a sink the team cannot retract, the sink has a reader set the team cannot enumerate, and the reader set persists for the sink’s retention window. The response is rotation, and the response is immediate.
The sinks the masker does not cover
The CI log is one of several sinks a workflow step can write to. The masker covers the CI log. The masker does not cover the workspace file, the artifact, the webhook, the database, the third-party error response, or the child process. A secret that reaches any of these sinks has been disclosed to that sink’s reader set.
flowchart LR
JOB["Step holds secret"] --> L["CI log\n(masker applies)"]
JOB --> W["Workspace file\n(uploaded as artifact)"]
JOB --> H["Webhook call\n(third-party reads)"]
JOB --> D["Database write\n(DBAs and logs read)"]
JOB --> C["Child process\n(inherits environment)"]
JOB --> E["Error response\n(sanity to third party)"]
L --> READER1["Team reads CI log"]
W --> READER2["Artifact readers read workspace"]
H --> READER3["Third party stores request"]
D --> READER4["DBA and DB audit read row"]
C --> READER5["Child output appears in parent log"]
E --> READER6["Third-party error tracker stores body"]
Each sink has a reader set, a retention window, and a discovery model. The CI log’s reader set is the team, the retention is the forge’s policy, and the discovery model is search and grep. The artifact’s reader set is whoever has download permission, the retention is the artifact bucket policy, and the discovery model is artifact listing. The webhook’s reader set is the third-party service and its employees, the retention is the third-party’s policy, and the discovery model is subpoena, breach, or audit.
The team can enumerate the CI log’s reader set with some confidence. The team cannot enumerate the third-party service’s reader set at all.
Patterns that reliably leak
Several script patterns leak secrets with high reliability. Recognising the patterns is the first step; rewriting them is the second.
Debug and verbose flags
A debug flag, a verbose flag, or a trace flag on a tool that takes a secret as input is a flag that prints the secret in the tool’s startup banner, in the tool’s request log, or in the tool’s error output.
curl -v -H "Authorization: Bearer $DEPLOY_TOKEN" "$API_URL"
The -v flag prints the request line, including the
header. The masker sees the line and masks the literal
token. But the verbose flag also prints connection-level
metadata - TLS handshake, response headers - that the
operator may copy into a bug report. The token is masked
in the platform log but unmasksed in the bug report.
Stack traces that quote the env
A library that prints the environment on an unhandled exception prints every environment variable, including the secret, into the stack trace. The trace is forwarded to an error tracker; the tracker stores the trace; the team finds the trace in the tracker.
raise RuntimeError("Failed with env: " + repr(os.environ))
The pattern is a debugging convenience that becomes a disclosure path.
Command-line argument echoing
A shell wrapper that prints the command line before
executing it prints every argument, including arguments
that contain the secret. The set -x shell option, the
xtrace option, and any wrapper that calls echo "$@"
before invoking the underlying tool are patterns that
echo the secret.
set -x
aws s3 sync ./build s3://"$BUCKET_NAME" --profile "$AWS_PROFILE"
set -x prints every command, including the value of
every variable expansion. The masker may or may not catch
the expansion depending on how the command line is
constructed.
Workflow files that print “context”
A workflow step that prints the run context for debugging prints the event payload, the env file, the matrix configuration, and the secret names (not values, but the secret names confirm the presence of a secret the attacker can target). The context dump is a reconnaissance artifact for an attacker who has read access to the log.
echo "$GITHUB_CONTEXT"
The context dump does not print values, but it prints the shape of the workflow’s authorisation model.
Workspace and artifact leak
The workspace is a directory the runner shares across
steps. A file the job writes to the workspace is a file
the artifact uploader uploads unless the workflow
explicitly excludes it. A ~/.aws/credentials file the
job materialises is a file the artifact uploader
uploads; the artifact bucket is a long-retention sink
whose reader set is the artifact permission policy.
mkdir -p ~/.aws
echo "[default]" > ~/.aws/credentials
echo "aws_access_key_id = $AWS_ACCESS_KEY_ID" >> ~/.aws/credentials
echo "aws_secret_access_key = $AWS_SECRET_ACCESS_KEY" >> ~/.aws/credentials
aws s3 sync ./build s3://"$BUCKET_NAME"
The credential file lives in the workspace for the
lifetime of the job. If the workflow uploads ** or any
path that includes the home directory, the credential file
is in the artifact.
Webhook and error response leak
A workflow step that calls a webhook or an API may receive an error response that contains the credential. The error response is logged by the job (or by the underlying library) and the masker does not cover the third-party response.
RESPONSE=$(curl -s -X POST -H "Authorization: Bearer $DEPLOY_TOKEN" "$API_URL")
echo "Response: $RESPONSE"
A 500 from the API whose body is {"error": "Internal error during request with token $DEPLOY_TOKEN"} is a body that contains the credential.
The masker sees the line and masks the literal token,
but if the response is forwarded to a third-party error
tracker, the tracker’s storage of the response is
outside the masker’s reach.
Child process and environment inheritance
A child process the job spawns inherits the environment. The child process writes to its own logs; those logs are not the platform’s log stream; the masker does not cover the child’s output unless the child is wired into the parent’s stdout.
ENV_VAR_WITH_SECRET=$DEPLOY_TOKEN /opt/third-party/tool --debug
The third-party tool’s debug output is a stream the masker may not be wired to. The tool writes the secret to its own log file; the file is on the runner; the runner’s workspace is uploaded as an artifact; the artifact contains the secret.
The rotation response
The response to a log leak is not “find the log and delete it”. The response is rotation.
flowchart LR
A["Leak detected"] --> B["Identify secret value"]
B --> C["Rotate at the source\n(vault, IAM, registry)"]
C --> D["Overwrite CI secret\nwith new value"]
D --> E["Audit all sinks\nfor residual value"]
E --> F["Review workflow\nfor the leak pattern"]
F --> G["Add detection\nfor future leaks"]
The reasoning:
- A leaked secret cannot be unleaked. The log’s retention window has started; the artifact’s reader set has been granted access; the webhook has received the request. Deleting the log at the source does not retract the disclosure.
- A rotated secret invalidates the leaked value. The attacker who holds the leaked value holds a credential the team has revoked. The attack window is closed at the source.
- The leak pattern must be fixed. A workflow that leaks once will leak again. The fix is structural, not cosmetic.
- The detection rule must be added. A pre-commit hook, a pre-receive hook, a workflow linter rule, or a secret scanner rule that catches the pattern prevents the next leak.
The rotation response is the only correct response. The “delete the log” response is theatre.
Production discipline
- Audit the workflow file for the four leak patterns before the workflow runs. Verbose flags, stack traces, command-line echoing, context dumps.
- Audit the workspace before the artifact upload. Any file in the workspace is in the artifact unless the workflow explicitly excludes it. The default for a secret is not to write it to a file.
- Sanitise third-party responses before they reach the log. A 500 with a credential in the body is a leak; the masker does not cover the third-party sink.
- Rotate on detection, not on suspicion. A leaked secret is rotated at the source; the CI secret is overwritten; the new value is verified.
- Add detection for the leak pattern. A secret scanner rule, a linter rule, a pre-commit hook that catches the pattern prevents the next incident.
Cross-course references
- Git, CI/CD & GitOps — Part XXXV-03 (The rotation response) covers the rotation protocol for a leaked secret in detail.
- Git, CI/CD & GitOps — Part XXXVIII-06 (Artifacts, caches, and outputs) covers the workspace-to-artifact leak path.
- Git, CI/CD & GitOps — Part XLI-04 (The Docker socket risk) covers the malicious-step pattern that turns any sink into an attacker-controlled exfiltration path.
Quiz
Knowledge check · 4 questions
Q1. A workflow step writes a credential to a file in the workspace so the AWS CLI can read it. The workflow uploads the workspace as an artifact. What is the leak status?
Q2. A secret that has been printed once to a CI log is recoverable by deleting the log line.
Q3. Name three script patterns that reliably leak secrets to log sinks, and explain why each bypasses the masker.
Q4. Diagnose the leak path, identify all the sinks the secret reached, and prescribe the rotation response.
Team T's deploy workflow runs a step that calls `set -x` for debugging, then invokes the deploy tool with an OIDC-issued STS token in the environment. The step's output is forwarded to a third-party error tracker that captures every log line. A teammate onboards two weeks later and is granted read access to the team's log archive by default. An audit firm is contracted six weeks later and receives a copy of the log archive as part of the SOC2 evidence package. The STS token has a one-hour lifetime and was rotated at the end of the job, but the log contains the literal token value at the moment of issue.
Passing score: 75%. Answers are checked in this browser.