Assemble the stages into one enforced gate, then secure the pipeline itself — least-privilege tokens, pinned actions, and OIDC instead of long-lived cloud keys. The CI/CD system is now your highest-value target.
Scanners produce findings; a gate turns findings into a pass/fail decision. Centralize the policy — which severities block, which are warnings, what is temporarily accepted — so it is explicit and consistent rather than scattered across tool configs. The gate is where security and delivery negotiate: strict enough to matter, pragmatic enough to ship.
Gate policy (make it explicit and versioned):
BLOCK the release on:
- any secret detected
- new HIGH/CRITICAL CVE with a fix available
- new high-confidence SAST finding (injection, authn, crypto)
- DAST: auth bypass, injection, missing critical headers
WARN (don't block) on:
- medium/low findings, unfixable CVEs (track them)
ACCEPT (time-boxed, signed off, in the repo):
- specific CVE/rule with a reason + expiry date
A build that goes green must MEAN the policy passed.Policy is only real if the platform enforces it. Use branch protection to require the security jobs to pass before merge, and a protected deployment environment (optionally with human approval) so nothing reaches production without clearing the gate. This is the difference between advisory scanning and a control.
# the gate job depends on every scan — one red scan fails the gate
gate:
needs: [secrets, sast, deps, image-scan, dast]
runs-on: ubuntu-latest
steps:
- run: echo "All security checks passed — cleared to deploy."
deploy:
needs: [gate]
environment: production # protected env: approvals + secrets scoped here
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
# In repo settings: mark 'gate' a REQUIRED status check on main.
# Now merge/deploy is impossible while any check is red.A CI/CD pipeline builds and deploys with high privilege, has access to secrets, and runs code from every pull request — which makes it one of the most attractive targets in your estate (see SolarWinds). Apply least privilege to the workflow token, pin third-party actions to a commit SHA so a compromised tag cannot inject code, and never expose secrets to untrusted PR runs.
# secure the workflow the same way you secure prod
permissions:
contents: read # default: minimal. grant more per-job only if needed.
jobs:
build:
permissions:
contents: read
id-token: write # for OIDC (next topic), not a static key
steps:
# pin actions to a full commit SHA, not a movable tag like @v4:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# untrusted PRs run with read-only token + no secrets by default —
# keep it that way; never run deploy steps on 'pull_request' from forks.The worst secret to store in CI is a long-lived cloud credential — if the pipeline is compromised, so is your cloud account. Federated identity (OIDC) lets the pipeline exchange a short-lived, workflow-scoped token for cloud access at runtime, so there is no standing secret to steal. This is the single highest-leverage hardening step for a deploy pipeline.
# GitHub Actions -> AWS with NO stored access keys
deploy:
permissions:
id-token: write # let the job mint an OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy-from-ci
aws-region: us-east-1
# no aws_access_key_id / secret here — GitHub's OIDC token is
# exchanged for short-lived creds, scoped to this repo + branch.
# The trust policy on that IAM role restricts it to your repo:
# "token.actions.githubusercontent.com:sub": "repo:org/app:ref:refs/heads/main"
# A stolen pipeline can't get creds outside that narrow condition.