Skip to content
C Codeloom
CI/CD

CI/CD Secrets Management Best Practices

Keep API keys, tokens, and database credentials safe in CI/CD with rotation, scoping, secret managers, and OIDC-based authentication.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Where secrets typically leak in pipelines
  • How to scope and rotate secrets
  • Using secret managers from CI
  • OIDC for keyless cloud auth
  • Detecting accidental commits of secrets

Prerequisites

  • Comfortable running a CI pipeline

What and Why

CI runs with elevated permissions. It can deploy to production, push images, and call third-party APIs. That makes pipelines a juicy target. Treating secrets carelessly here is one of the fastest ways to lose data, take down services, or get billed for someone else’s crypto mining. Good practices keep secrets short-lived, narrowly scoped, and never embedded in source.

Mental Model

Think of secrets as money. The less you carry, the less you lose. Hand each job only what it needs, refresh frequently, and never store them in plaintext where the wrong eyes can see them. Better still, swap long-lived secrets for short-lived tokens issued at job start.

CI job starts
   |
   v
Authenticate to secret manager (OIDC or token)
   |
   v
Fetch only needed secrets at runtime
   |
   v
Use them in memory
   |
   v
Job ends -> tokens expire, secrets discarded
Secret flow with a secret manager

Hands-on Example

GitHub Actions with environment-scoped secrets.

# .github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh

Use OIDC to authenticate to AWS without long-lived keys.

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/ci-deployer
          aws-region: us-east-1
      - run: aws s3 sync ./out s3://my-bucket

The CI job gets a short-lived AWS session, scoped to a role. No AWS_ACCESS_KEY_ID stored anywhere.

Fetch secrets from a manager at runtime.

- name: Read DB password
  run: |
    PASSWORD=$(aws secretsmanager get-secret-value \
      --secret-id prod/db --query SecretString --output text)
    echo "::add-mask::$PASSWORD"
    echo "DB_PASSWORD=$PASSWORD" >> $GITHUB_ENV

::add-mask:: ensures the value is redacted from logs.

Scan history for accidentally committed secrets in pre-commit and CI.

- uses: gitleaks/gitleaks-action@v2
  with:
    config-path: .gitleaks.toml

Rotate secrets on a schedule or trigger. With a secret manager, rotation is a click or an API call away.

Common Pitfalls

  • Echoing secrets in logs. Even partial values can be enough for an attacker.
  • Using one root token for everything. A leak compromises the whole environment.
  • Long-lived cloud credentials checked in or stored in plain GitHub secrets without rotation.
  • Sharing CI secrets with forks or pull requests from external contributors.
  • Not auditing access. Secret managers track who reads what; use it.

Practical Tips

  • Prefer OIDC federation over storing static cloud keys.
  • Use environment scoping. Production secrets belong to a protected environment with required reviewers.
  • Mask secrets explicitly so they do not appear in set -x output.
  • Add secret scanning to both pre-commit and CI. Treat findings as incidents.
  • Document a clear rotation cadence and own the process. Rotation no one runs is fictional security.

Wrap-up

Secrets in CI deserve the same care as production credentials in your runtime, because effectively they are. Move to OIDC where possible, scope every secret to the smallest set of jobs that need it, rotate aggressively, and audit who reads what. With those habits, a single mistake in a pipeline is contained instead of catastrophic.