CI/CDIntermediate 13 min Lesson 6 of 8

Secrets in CI/CD

Give pipelines the credentials they need without leaking them: scoping, masking, short-lived tokens and what to do after exposure.

CI/CD · Lesson 6 of 8
0/8 done(0%)

What is it? #

Pipelines need credentials: registry tokens, deployment keys, cloud access, API keys for tests.

Those credentials are attractive targets, because a pipeline with production access is effectively production access.

The principles are the same as everywhere else, applied to an environment that runs code from pull requests: least privilege, short lifetimes, scoping by environment, and never in the repository.

The strongest available improvement is replacing long-lived keys with short-lived tokens obtained at run time.

Think of it like this #

Giving a contractor a key that opens one door, works for one afternoon, and logs every use.

Handing over a master key that never expires is quicker and means an ex-contractor still has access a year later.

Simple example #

A pipeline that deploys to production. It uses a scoped registry token, a deployment key limited to one command, and short-lived cloud credentials obtained per run rather than stored.

Code #

YAML
# Scoping: production secrets belong to the production environment
jobs:
  deploy-production:
    environment: production        # only this environment's secrets are available
    steps:
      - run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}
# A job that does not declare the production environment cannot read those secrets,
# including anything triggered from a fork's pull request.
YAML
# The best option: short-lived credentials via OIDC, nothing stored
jobs:
  deploy:
    permissions:
      id-token: write              # request an identity token
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/deploy-role
          aws-region: eu-west-1
# The cloud provider trusts this repository and workflow, and issues
# credentials valid for minutes. There is no long-lived key to leak.
YAML
# Least privilege for the built-in token
permissions:
  contents: read                   # default to read-only
  packages: write                  # only what this job needs
  id-token: write
BASH
# Handling secrets inside a job
echo "$REGISTRY_TOKEN" | docker login ghcr.io -u ci --password-stdin   # not on the command line
echo "::add-mask::$DERIVED_VALUE"          # mask a value computed at run time

# Never do this
# echo "Deploying with token $DEPLOY_TOKEN"       # printed into the log
# curl -H "Authorization: Bearer $TOKEN" ... -v   # verbose prints headers
# env                                              # dumps everything
TEXT
Pull requests from forks

A fork's pull request runs workflow code that a stranger wrote. If it
could read your secrets, it could print them.

Platforms restrict this by default: secrets are not available to fork
pull requests. Do not work around it. If a check genuinely needs
credentials, run it after review, on a separate trusted trigger.
TEXT
After a secret is exposed

1. rotate it immediately — this is the only step that actually helps
2. check the audit logs for use between exposure and rotation
3. remove it from the code and the history
4. add a scanner to the pipeline so it is caught next time

Deleting the commit does nothing. Assume it was captured.

How it works #

Environment-scoped secrets are only readable by jobs declaring that environment, which means a test job cannot read production credentials even accidentally.

OIDC-based credentials are the significant improvement. The cloud provider trusts a specific repository and workflow, and issues credentials valid for minutes. There is no stored key, so there is nothing to leak or rotate.

Narrowing permissions limits the built-in token. The default is broader than most jobs require, and reducing it is a one-line change.

Masking prevents accidental printing. Platform-provided secrets are masked automatically; values derived at run time are not, which is why add-mask exists.

Passing tokens via stdin rather than on the command line keeps them out of process listings and logs, which frequently capture commands.

Verbose flags are a common leak. curl -v prints request headers, including the authorization header, straight into the build log.

The fork restriction exists because a pull request can modify the workflow file. Working around it by running fork code with secrets available has caused real compromises.

Rotation is the only response to exposure that matters. The secret must be assumed captured the moment it was visible.

Real-world use #

CI systems are a recognised supply-chain target. A pipeline with production credentials is an attractive route in, and compromised build systems have been used to reach production more than once.

OIDC is now well supported by the major cloud providers and is the recommended approach, replacing stored access keys entirely.

Third-party actions run with access to your job. Pinning them to a commit SHA rather than a moving tag prevents a compromised update running in your pipeline.

Secret scanning in the pipeline catches credentials before they are committed, and most platforms also scan pushed code and alert on findings.

Rotation should be routine rather than incident-driven. Credentials belonging to people who have left, or keys older than a year, are worth replacing on a schedule.

Common mistakes #

  • Long-lived cloud keys stored as secrets when OIDC is available.
  • Secrets available to every job rather than scoped to an environment.
  • Printing tokens through verbose flags or debug output.
  • Using floating tags for third-party actions instead of pinned commits.
  • Deleting an exposed secret from the code without rotating it.

Practice #

Audit a pipeline: list every secret, what uses it, and what it grants. Scope production secrets to a production environment, narrow the token permissions, and replace one long-lived credential with a short-lived one if your provider supports it.

Quick quiz

  1. 1. What is the strongest improvement over stored credentials?

  2. 2. Why scope secrets to an environment?

  3. 3. Why are secrets unavailable to fork pull requests by default?

  4. 4. What is a common accidental leak in build logs?

  5. 5. What is the only effective response to an exposed secret?

Summary

  • Prefer short-lived OIDC credentials over stored long-lived keys.
  • Scope secrets to environments and narrow token permissions.
  • Pass tokens via stdin and never enable verbose output with credentials.
  • Do not give fork pull requests access to secrets.
  • Rotate immediately after any exposure — deleting the commit does nothing.