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+NEXTfor 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
“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 |
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
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.
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 077andtrapcleanup - Prefer environment-scoped secrets (dev/staging/prod)
Avoid
set -xnear 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)
- Create a new credential with the same scope as the old one
- Store it as
NEXT(or new version) in your secrets store - Deploy a change that can use
NEXT(keepACTIVEas fallback) - Verify key operations (deploy, API calls, signing) and monitor errors
- Promote
NEXTtoACTIVE - 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).
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_*vsSECRET_*.
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
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.