Handling Secrets in CI Without Leaking Them
Introduction¶
Every CI system lets you store secrets and inject them as environment variables. The danger isn't storage — it's the dozen ways a secret escapes at runtime.
How secrets leak¶
set -x/ debug mode printing the command line with the value in it.echo "$TOKEN"for debugging, left in.- A tool printing the value in an error message ("auth failed for token ghp_xxx").
- Build artifacts — a
.envfile, a compiled config, a Docker layer (ARG SECRETis recoverable from history). env/printenvdumped in a "collect diagnostics" step.- Forked pull requests — a malicious PR editing the workflow to exfiltrate secrets.
- Cache — a secret written into a cached directory, restored on another branch.
Masking and its limits¶
GitHub Actions (and GitLab, etc.) mask registered secrets in logs — occurrences
are replaced with ***. Limits:
- Only exact string matches. A base64 or URL-encoded form of the secret isn't masked. A secret split across lines isn't masked.
- Multi-line secrets (private keys) mask poorly; register each line, or better, write to a file.
- Masking is log-only. Artifacts, caches, and outbound network calls are not touched.
add-maska value you compute at runtime:echo "::add-mask::$DERIVED".
Treat masking as a safety net, not a control.
OIDC: stop storing cloud keys¶
The biggest win. Instead of a long-lived AWS_ACCESS_KEY_ID secret, the CI job
requests a short-lived token from your cloud via OpenID Connect federation:
permissions:
id-token: write # allow the job to request an OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deployer
aws-region: eu-west-1
The cloud trust policy restricts which repo, branch, and environment can assume the role. No secret to leak, credentials expire in an hour, and every assumption is logged in CloudTrail. GCP and Azure have equivalents.
Least privilege and environments¶
- Scope secrets to environments (
production) with required reviewers, so a deploy secret isn't available to every PR run. - Give the CI role only the permissions the pipeline needs.
- Separate read (build/test) from write (deploy) credentials; most jobs need read only.
Forked PR safety¶
pull_requestfrom a fork runs with no secrets and a read-only token by default — keep it that way.- Never use
pull_request_target(which does have secrets) to run untrusted PR code. If you need labels/comments on forked PRs, do it in a separate workflow triggered byworkflow_runthat doesn't check out the PR head. - Require approval for first-time contributors' workflow runs.
Verification and troubleshooting¶
# Scan the repo + history for committed secrets
gitleaks detect --source . --redact
trufflehog git file://. --only-verified
# Inspect a built image for baked-in secrets
docker history --no-trunc myimage
dive myimage
- Secret appears as
***but the deploy still fails "invalid credentials" — a trailing newline in the stored secret, or it's expired/rotated. Re-store withprintf %ssemantics. - Secret visible despite masking — it was transformed (base64, json-encoded) before printing. Mask the transformed form too, or don't print it.
configure-aws-credentialsfails "Not authorized to perform sts:AssumeRoleWithWebIdentity" — the IAM trust policy'ssubcondition doesn't match (repo:org/name:ref:refs/heads/mainvs environment form), orid-token: writepermission is missing.- Fork PR can't run needed checks — that's the design; move secret-requiring
steps to a post-merge or
workflow_runjob. - Old access keys still active — after moving to OIDC, delete the IAM user / rotate out the stored secret. It's not gone until you remove it.
Related tools and reading¶
- On-site: JWT decoder, Base64 decoder.
- Related posts: GitHub Actions cache strategies, ConfigMaps and Secrets done right.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in DevOps & CI/CD
OpenTelemetry for Distributed Tracing
A trace follows one request across services. OpenTelemetry is the vendor-neutral way to produce them — here's the model, the Collector, and sampling that won't bankrupt you.
October 24, 2026pre-commit Hooks That Actually Help
pre-commit runs checks before a commit lands, catching formatting, secrets, and syntax errors locally. Here's a config that helps without being annoying.
October 24, 2026Chaos Engineering Basics
Chaos engineering is controlled experiments that inject failure to verify your system tolerates it. Here's the method, safe blast radius, starter experiments, and game days.
October 24, 2026