Cloud & DevOps · Secrets

Secrets in CI/CD: The Right Way to Store and Rotate Keys

A practical workflow for teams and automation.

Reading time: ~8–12 min
Level: All levels
Updated:

Secrets in CI/CD are a weird mix of “invisible” and “high-impact”: one leaked token can turn a clean pipeline into a supply-chain incident, but you often only notice after the fact (a suspicious deploy, a drained cloud bill, or a revoked credential). This guide is a practical workflow you can apply to any CI system to store secrets safely, reduce blast radius, and rotate keys without breaking builds.


Quickstart

If you only do a few things, do these first. They’re the highest-leverage changes for secrets in CI/CD, and they scale from a solo repo to a large org.

1) Stop using long-lived cloud keys in CI

Prefer short-lived credentials (OIDC / workload identity) so the runner can exchange a signed token for temporary access. If a token leaks, it expires quickly.

  • Use OIDC federation to your cloud/provider
  • Restrict trust by repo + branch + environment
  • Keep permissions minimal (one job, one purpose)

2) Put secrets in a secret store, not in YAML

Your pipeline config should reference secrets by name (or path), not contain secret values. Use your CI platform’s secrets UI/CLI or a dedicated secrets manager.

  • No secrets in repo (including “temporary”)
  • No secrets in build artifacts
  • No secrets passed via command-line args

3) Add a rotation path with overlap

Rotation shouldn’t be an emergency-only action. Make it routine using a two-version approach: create a new credential, deploy with both accepted, then revoke the old one.

  • Support ACTIVE + NEXT for a short window
  • Verify production behavior before revoking
  • Log rotation events (who/what/when)

4) Make it hard to leak (logs, forks, debug)

Most leaks happen through logging and debugging, not through “hackers.” Your job is to make leaks harder than doing the right thing.

  • Disable shell tracing around secret usage
  • Use masked variables and avoid echoing values
  • Restrict secrets on PRs from forks
The goal

“Secrets in CI/CD” is really about controlling where secrets exist (and for how long). Aim for: central storage + least privilege + short lifetimes + easy rotation.

Overview

CI/CD is a privileged automation layer: it pulls source, builds artifacts, and often has the power to deploy to production. That makes CI secrets (API keys, signing keys, cloud credentials, webhook secrets) a high-value target. If an attacker gets one credential with broad permissions, they can pivot into your infrastructure.

What this post covers

  • Where to store secrets (CI secret store vs dedicated secrets manager vs KMS)
  • How to inject secrets safely into jobs without leaking to logs or artifacts
  • How to rotate keys with minimal downtime using overlap and versioning
  • How to reduce blast radius with least privilege and short-lived credentials
  • Common mistakes teams make (and how to fix them quickly)
Secret type Best practice in CI/CD Rotation expectation
Cloud access (deploy, infra) Prefer OIDC/workload identity (no static keys) Automatic (short-lived) or periodic policy review
API tokens (third-party services) Store in secrets manager; scope to smallest permissions Monthly/quarterly (or per vendor guidance)
Signing keys (artifacts/images) Use KMS/HSM-backed signing when possible; restrict access Planned rotations; emergency revoke path
DB credentials Prefer dynamic DB creds (Vault-like) or short TTL tokens Automated where possible; otherwise scheduled
Fast mental model

Ask two questions for every secret: “Where does it live?” and “How do we rotate it?” If either answer is unclear, that’s the next thing to fix.

Core concepts

Good secrets hygiene isn’t “never leak.” It’s designing your automation so that when something goes wrong, the impact is limited, detectable, and recoverable.

Secret vs credential vs key

What a “secret” means in CI

Any value that grants access or proves identity: API tokens, private keys, webhook secrets, registry creds, database passwords, service account JSON, session cookies.

  • Secret: the sensitive value
  • Credential: the thing used to authenticate (often contains secrets)
  • Key: a specific credential type (API key, private key, access key)

Why “CI/CD secrets” are special

Pipelines run unattended, often with high privileges, and they touch your source code and release process. If a secret leaks here, it can become a supply-chain problem.

  • Secrets can leak via logs, artifacts, caches
  • PRs from forks can trigger untrusted code
  • Runners may be shared or reused

Blast radius, least privilege, and time-to-live

When you can’t eliminate risk, you shape it: blast radius is “what can this credential do,” and TTL is “for how long.” The best CI setups shrink both.

Control What it reduces How it looks in practice
Least privilege Blast radius Deploy role can deploy, but cannot read prod DB
Short-lived creds Time window OIDC exchange for 15–60 minute tokens
Environment scoping Accidental misuse Prod secrets only available to “production” workflow
Audit logging Detection time See who accessed/changed secrets and when

Rotation vs revocation vs rollover

Rotation

Regularly replacing secrets to reduce exposure and enforce hygiene.

  • Planned schedule (monthly/quarterly)
  • Automated where possible
  • Change tracking + verification

Revocation (emergency)

Immediately invalidating a credential you suspect is compromised.

  • Assume it leaked if it hits logs
  • Revoke first, investigate second
  • Rotate dependent secrets next
Rotation that breaks builds isn’t rotation

If rotation causes downtime, teams avoid it until an incident forces their hand. Design rotation as a rollover: allow old + new briefly, deploy with new, then revoke old.

Step-by-step

This is a practical workflow you can implement incrementally. You don’t need to adopt every tool on day one. The key is to build a clear path from “secret exists” → “pipeline uses it” → “we can rotate it safely.”

Step 1 — Inventory secrets and classify them

Start with a simple spreadsheet or document. List every secret used by CI, where it is stored, what it is used for, and who owns it. Classification is how you decide what to fix first.

Minimum inventory fields

  • Name: “prod-deploy-role”, “npm-token”, “webhook-signing-secret”
  • Scope: repo / environment / service
  • Permissions: what actions it can perform
  • TTL: long-lived vs short-lived
  • Rotation: how and how often
  • Owner: team/person responsible

Step 2 — Choose a storage pattern (and be consistent)

Most teams use a combination: the CI platform secret store for small and simple secrets, and a dedicated secrets manager for shared, rotated, or high-sensitivity secrets.

Where to store Best for Watch out for
CI secret store (repo/org/env secrets) Small set of secrets scoped to a repo; fast to start Rotation at scale can be harder; audit features vary
Secrets manager (centralized) Shared secrets, automation, rotation, access policies Requires access model and integration work
KMS/HSM (key services) Signing/encryption keys, high assurance Do not export private keys unless you must

Step 3 — Prefer short-lived credentials (OIDC/workload identity)

This is the single biggest “secrets in CI/CD” upgrade: instead of storing a static cloud key, your job requests a signed identity token from the CI provider and exchanges it for temporary access. No long-lived key to leak, and every run can be audited.

How this helps rotation

When you move from static keys to short-lived credentials, rotation becomes mostly a policy problem (review roles and permissions) rather than a “change secret values everywhere” problem.

Example: a workflow that uses OIDC to obtain short-lived cloud credentials (no stored cloud keys). The exact action/provider names vary by platform, but the pattern is the same: trust the repo + branch, assume a role, do the deploy.

name: deploy

on:
  push:
    branches: [ "main" ]

permissions:
  id-token: write   # required for OIDC token minting
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Exchange the CI OIDC token for short-lived cloud credentials.
      - name: Configure cloud credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ci-deploy-role
          aws-region: eu-central-1

      - name: Deploy
        run: |
          set -euo pipefail
          ./scripts/deploy.sh

Step 4 — Lock down the trust relationship (don’t “trust all repos”)

OIDC is only as safe as the trust policy behind it. Tighten trust conditions so only the right repository, branch, and environment can assume the deployment role.

Example: an AWS IAM role trust policy for CI OIDC federation that restricts who can assume it. This is a Terraform snippet you can adapt (the values are placeholders).

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = ["sts.amazonaws.com"]

  thumbprint_list = [
    # Use the correct thumbprint for your environment/provider.
    "6938fd4d98bab03faadb97b34396831e3780aea1"
  ]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    # Restrict by repository and branch:
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [
        "repo:your-org/your-repo:ref:refs/heads/main"
      ]
    }

    # Restrict audience:
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ci_deploy_role" {
  name               = "ci-deploy-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

# Attach only the permissions required for deployment (least privilege).
resource "aws_iam_role_policy" "deploy_policy" {
  name = "deploy-policy"
  role = aws_iam_role.ci_deploy_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "s3:PutObject",
          "s3:ListBucket"
        ],
        Resource = [
          "arn:aws:s3:::your-artifact-bucket",
          "arn:aws:s3:::your-artifact-bucket/*"
        ]
      }
    ]
  })
}

Step 5 — Inject secrets safely into the job (and clean up)

Even if your secret is stored correctly, you can still leak it during execution. The safest pattern is: fetch/inject secrets at runtime, keep them in-memory when possible, and write to disk only when necessary (with strict permissions and cleanup).

Do

  • Use masked secrets (CI feature) for logs
  • Keep secrets out of command-line args
  • Write temp files with umask 077 and trap cleanup
  • Prefer environment-scoped secrets (dev/staging/prod)

Avoid

  • set -x near secret handling (it prints commands)
  • Echoing secret values for debugging
  • Saving secrets into build caches
  • Passing secrets to untrusted PR builds

Step 6 — Rotate keys using “overlap” (the safe rollout)

The most reliable rotation pattern is a staged rollover: you introduce a new credential while the old one is still valid, switch usage to the new one, verify, then revoke the old credential. This prevents sudden outages.

A rotation playbook (generic)

  1. Create a new credential with the same scope as the old one
  2. Store it as NEXT (or new version) in your secrets store
  3. Deploy a change that can use NEXT (keep ACTIVE as fallback)
  4. Verify key operations (deploy, API calls, signing) and monitor errors
  5. Promote NEXT to ACTIVE
  6. Revoke the old credential and remove it from storage

Example: a bash snippet that safely uses a secret (as a file) and supports an overlap window (ACTIVE/NEXT). The “get_secret_*” commands are placeholders for your CI or secrets manager integration.

#!/usr/bin/env bash
set -euo pipefail

# Avoid printing commands that might include secrets.
set +x

umask 077
tmpdir="$(mktemp -d)"
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT

# Fetch secrets from your secret store (placeholders).
ACTIVE_TOKEN="$(get_secret_value service/token/ACTIVE || true)"
NEXT_TOKEN="$(get_secret_value service/token/NEXT || true)"

# Prefer NEXT during rotation, fall back to ACTIVE.
TOKEN="${NEXT_TOKEN:-$ACTIVE_TOKEN}"

if [[ -z "${TOKEN}" ]]; then
  echo "ERROR: No token available (ACTIVE/NEXT missing)." >&2
  exit 1
fi

token_file="$tmpdir/token"
printf '%s' "$TOKEN" > "$token_file"

# Use the secret via file (safer than args; still avoid echoing).
curl --fail --silent --show-error \
  -H "Authorization: Bearer $(cat "$token_file")" \
  "https://api.example.internal/health" >/dev/null

echo "OK: token works (rotation overlap supported)."

Step 7 — Add audit and “break-glass” response

Make it easy to answer: “Who changed this secret?” and “Which pipelines used it?” Also define what happens when a secret leaks (because eventually something will).

If a secret is in logs, treat it as compromised

Don’t debate probability. Rotate/revoke immediately, then investigate the source. The fastest incident is the one you can recover from without heroics.

Minimum incident checklist

  • Revoke/rotate the leaked credential
  • Invalidate sessions/tokens derived from it (if applicable)
  • Review recent access logs and deployments
  • Check for persistence (new keys/users/roles created)
  • Patch the leak source (debug logs, scripts, CI config)

Common mistakes

These are the patterns behind “we leaked a token” and “rotation broke production.” Fixing them usually takes less time than you think.

Mistake 1 — Secrets committed to Git (even once)

History is forever. Even if you revert, clones and caches may still contain the value.

  • Fix: rotate/revoke immediately, then scrub history if needed.
  • Fix: add pre-commit and server-side scanning; block merges on leaks.

Mistake 2 — One “god key” used by every pipeline

Convenient, until it isn’t. One leak becomes a full account compromise.

  • Fix: one role/credential per repo and environment.
  • Fix: split read-only vs deploy/write permissions.

Mistake 3 — Static cloud credentials in CI

Long-lived access keys are the #1 “easy win” to remove.

  • Fix: switch to OIDC/workload identity (short-lived tokens).
  • Fix: lock trust by repo/branch and keep policies minimal.

Mistake 4 — Secrets leaked through logs or tracing

Debug habits are responsible for a huge percentage of leaks.

  • Fix: avoid set -x; mask secrets; never print values.
  • Fix: pass secrets via env or temp files, not CLI arguments.

Mistake 5 — Secrets available to untrusted PRs

Forked PRs can run attacker-controlled code. Don’t hand it secrets.

  • Fix: restrict secrets to trusted branches/environments.
  • Fix: use separate “test-only” credentials with no write access.

Mistake 6 — Rotation with no overlap window

If you flip the secret once, you will break something eventually.

  • Fix: implement ACTIVE/NEXT versions and promote after verification.
  • Fix: automate checks that prove the new credential works.

Mistake 7 — No ownership or audit trail

“Who owns this token?” should never be a mystery during an incident.

  • Fix: assign owners and document rotation in the repo/security runbook.
  • Fix: enable audit logs for secret reads/changes where available.

Mistake 8 — Treating secrets as configuration

Configs can be public; secrets can’t. Mixing them creates accidental exposure.

  • Fix: separate config (non-sensitive) from secrets (sensitive) explicitly.
  • Fix: adopt naming conventions: CONFIG_* vs SECRET_*.

FAQ

Where should CI/CD secrets be stored?

Store secret values in a CI secret store (repo/org/environment secrets) or a dedicated secrets manager. Your pipeline config should only reference the secret by name/path. Never store secrets directly in YAML, code, or Git history.

Are environment variables safe for secrets in CI?

They can be safe enough if your CI platform masks them and you avoid printing them. The risk is accidental exposure via logs, subprocesses, or debug output. For extra safety, load secrets into a temp file with strict permissions and delete it after use.

How often should we rotate keys used in CI/CD?

Rotate based on risk and capability. For high-privilege credentials (deploy, signing), aim for regular rotation (monthly/quarterly) and a tested emergency revoke path. If you can switch to short-lived credentials (OIDC), you reduce the need for frequent value rotations because tokens expire automatically.

What’s the safest way to authenticate from CI to the cloud?

Use OIDC/workload identity to exchange a CI-issued identity token for short-lived cloud credentials. This avoids storing long-lived access keys in CI and sharply reduces the impact of accidental leaks.

How do we rotate a secret without breaking deployments?

Use a rollover: create a new credential, store it as NEXT, deploy code/config that prefers NEXT but can fall back to ACTIVE, verify production behavior, then revoke the old credential. Rotation should be boring and routine.

What should we do if a secret appears in logs?

Treat it as compromised. Revoke/rotate immediately, then investigate how it leaked and whether it was used. After containment, patch the leak source (debug output, tracing, scripts) so it can’t happen again.

Cheatsheet

A scan-fast checklist for secrets in CI/CD: storage, injection, and rotation.

Storage

  • Secrets live in CI secret store or a secrets manager
  • Environment scoping: dev/staging/prod secrets are separate
  • One credential per repo/service (avoid shared “god keys”)
  • Enable audit logs for secret reads/changes where possible

Access model

  • Prefer OIDC/workload identity (short-lived tokens)
  • Least privilege policies (deploy can deploy, nothing else)
  • Restrict trust by repo + branch + environment
  • Separate read-only workflows from deploy workflows

Safe usage in jobs

  • Mask secrets in logs; avoid printing values
  • Disable tracing around secret usage (set +x)
  • Prefer temp files with umask 077 + cleanup trap
  • Never pass secrets in command-line args

Rotation

  • Use ACTIVE/NEXT overlap window (rollover)
  • Verify the new credential in production before revoking old
  • Record owner + rotation cadence + last rotated date
  • Have an emergency revoke checklist (tested)
If you see… Do this now Then improve
Static cloud access key in CI Replace with short-lived identity Lock trust policy; least privilege roles
Secret in logs Revoke/rotate immediately Mask logs; remove debug/tracing; add checks
Rotation breaks builds Restore old temporarily if safe Add overlap (ACTIVE/NEXT) and verification step

Wrap-up

The “right way” to handle secrets in CI/CD is not one magic tool—it’s a workflow: keep secrets out of the repo, prefer short-lived credentials, restrict permissions, and make rotation routine. If you implement only one change, make it OIDC/workload identity for cloud access. It removes a whole class of long-lived keys that are easy to leak and painful to rotate.

What to do next (15 minutes)

  • Pick one pipeline that deploys to production
  • Inventory its secrets and remove any “god key”
  • Switch cloud auth to short-lived identity (OIDC) if possible
  • Add a rotation note: cadence + owner + rollback/overlap approach
A good sign you’re done

Rotation feels like a normal maintenance task, not a scary outage risk. When secrets are versioned and scoped, your CI/CD becomes easier to operate—and safer.

Quiz

Quick self-check. One correct answer per question.

1) Where should secret values live in a CI/CD setup?
2) What is the safest common pattern for CI to access cloud resources?
3) What’s the most reliable way to rotate a key without breaking production?
4) A secret value appears in CI logs. What should you do first?