Secrets are the reason CI/CD pipelines are worth attacking. A build job routinely holds cloud keys, registry tokens, and signing credentials that would trigger alarms anywhere else, and it is treated as "just automation." The scale of the exposure is documented: GitGuardian's 2025 State of Secrets Sprawl report detected more than 23 million new leaked secrets on public GitHub in 2024 alone, and that is only the public surface. The consequences are documented too. The January 2023 CircleCI breach exfiltrated customer environment variables and forced every customer to rotate every secret they had stored. The 2021 Codecov Bash Uploader compromise quietly exfiltrated environment variables and tokens from thousands of pipelines for roughly two months. And in 2022, Toyota disclosed that an access key had sat in a public GitHub repository for about five years, exposing data tied to hundreds of thousands of customers. The through-line: a stored secret is a liability, and the best-protected secret is the one that is short-lived, scoped, and never stored at all. This guide covers how to get there.
The attack surface
CI/CD secrets leak through four main channels. Source code: hardcoded keys committed to a repo, often in a config file, a test fixture, or a .env that was never meant to be pushed. Build logs: a secret echoed into a log where masking failed on a transformed value like base64. The platform: environment variables and contexts stored in the CI provider, exactly what the CircleCI breach exfiltrated. Blast radius: a single long-lived key that unlocks production, so one leak becomes a full compromise. Every control below attacks one of these channels.
Hardening step 1: scan for secrets before they land
Catch secrets at the earliest possible point — the developer's machine and the pull request — not weeks later in an audit. Run a secret scanner as a pre-commit hook and again as a required pipeline check so a leaked key cannot merge.
# .pre-commit-config.yaml - block secrets before they are committed
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
Pair the local hook with a server-side check in CI, because pre-commit hooks are advisory and a determined or careless developer can skip them. Scan history too — a rotated-but-still-committed key is a live credential until it is revoked.
Hardening step 2: kill long-lived credentials with OIDC
The single highest-leverage change is to stop storing static cloud keys and let each job mint a short-lived credential at runtime through OpenID Connect. There is nothing durable to leak, and a stolen token expires in minutes. GitHub Actions, GitLab CI, CircleCI, and Azure Pipelines all support OIDC federation to the major clouds.
# GitHub Actions: exchange an OIDC token for a short-lived AWS credential
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: us-east-1
The critical detail is the cloud-side trust policy: scope it to your specific repository and branch (or org and project) via the token's subject claim, so a fork or another tenant cannot assume the role. A credential valid for ninety seconds and usable only by one branch is a fundamentally different risk than a static key valid for ninety days.
Hardening step 3: scope, rotate, and vault what remains
Some secrets cannot be federated — a third-party API token, a database password. For those:
- Scope narrowly. Prefer per-environment or per-job secrets over organization-wide ones, so a leak exposes the least possible surface.
- Back them with a vault. Store the value in HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault and pull it at job start, so the secret does not live in the CI platform itself.
- Rotate on a schedule and maintain an inventory, so a forced mass-rotation is a runbook rather than a scramble.
- Never echo a secret. Log masking is best-effort and string-match based; it misses base64, URL-encoded, and concatenated values. Pass secrets through environment variables or process arguments, not logged command lines.
Adding Safeguard scanning to the pipeline
Secrets hygiene is half the story; the dependencies your pipeline builds are the other half. Add a step that runs the Safeguard CLI so software composition analysis runs alongside your secret scanning.
- name: Run Safeguard scan
env:
SAFEGUARD_TOKEN: ${{ secrets.SAFEGUARD_TOKEN }}
run: |
curl -sSfL https://get.safeguard.sh/install.sh | sh
safeguard scan --fail-on high --sbom cyclonedx
Safeguard's SCA engine prioritizes findings by reachability so the gate fails on the exploitable minority, and where a fix exists Auto-Fix opens the upgrade pull request. When a secret does leak, the Griffin AI investigation engine helps you trace what the exposed credential could reach.
Hardening checklist
- Scan for secrets in pre-commit hooks and as a required CI check, including history
- Replace static cloud keys with OIDC federation, scoped by repo/branch or org/project
- Prefer short token lifetimes; a 15-minute credential beats a 90-day key
- Scope secrets to the narrowest environment or job that needs them
- Back non-federated secrets with a dedicated vault, not the CI platform
- Rotate on a schedule; keep an inventory so mass rotation is a runbook
- Never echo secrets; masking is best-effort and misses transformed values
Frequently Asked Questions
If my CI provider encrypts stored secrets, why is OIDC better?
Because encryption at rest does not help when the platform's own trust is compromised, as the CircleCI breach showed — the attacker rode a valid session and read secrets the platform could decrypt. OIDC removes the stored secret entirely: there is nothing to exfiltrate, and any token that is captured expires in minutes.
Is masking enough to keep secrets out of logs?
No. Masking redacts exact string matches on a best-effort basis and routinely fails on transformed values such as base64 or URL-encoded forms, and on secrets assembled from concatenated parts. Treat masking as a safety net, not a control, and simply never write secrets to logs.
We have hundreds of stored secrets. Where do we start?
Start by inventorying them and ranking by blast radius — production cloud keys first. Federate everything that can be federated via OIDC, vault the rest, and set rotation schedules. The inventory itself is valuable: the teams that struggled most in the CircleCI mass-rotation were the ones that did not know what they had.
How does Safeguard help with secrets specifically?
Safeguard focuses on the dependency and supply chain side rather than being a standalone secrets manager, but it complements secret scanning: it ingests SBOMs, prioritizes exploitable findings, and its Griffin AI engine helps trace impact when a credential is exposed. See pricing for plan options.
To wire these controls into your pipeline, see Safeguard CLI, SCA, Auto-Fix, Griffin AI, pricing, and full setup steps in the documentation at docs.safeguard.sh.