CI/CD security gates that don't slow the team down
Most security gates in CI/CD pipelines fail one of two tests: they're so noisy nobody reads them, or they're so slow developers route around them. Here's the version I actually keep in production.
The honest reason security gates get ignored isn't culture — it's UX. A 12-minute scan that yells about a CVSS 4.3 issue in a transitive dev dependency is not a security control, it's a bikeshed generator. The first job is to build something engineers don't actively hate.
The three rules
- Fast or fail-soft. Anything over ~90 seconds runs in parallel or post-merge — never in front of a human waiting to ship a hotfix.
- Actionable or silent. If the finding doesn't tell the developer exactly what to change, it doesn't belong in the PR. Send it to the backlog instead.
- One owner per finding. "Security team" is not an owner. CODEOWNERS file, repo or service, name attached. No exceptions.
The pipeline shape
This is the GitHub Actions layout I default to. GitLab CI maps 1:1.
name: ci
on: [pull_request, push]
permissions:
id-token: write # OIDC for AWS, no static keys
contents: read
security-events: write
jobs:
lint-and-test: # ~1–2 min
sast: # Semgrep, ~30s
dependency-scan: # OSV-Scanner or Trivy fs, ~20s
build-image: # depends on lint-and-test
scan-image: # Trivy/Grype on the built image, ~30s
sbom: # Syft, attached to release
dast: # ZAP baseline, only on main, ~5 min
deploy: # OIDC-assumed role, only on main
SAST without the noise
Default Semgrep rulesets are fine to start, but they will fire on things your team will never fix. After 2 weeks, do a triage round:
- Anything you'd never block a PR on → demote to
WARNor remove. - Anything you'd always block on → promote to required check.
- Everything in between → write a one-line policy ("we accept JWT signing key in env if rotated by Secrets Manager") and add a Semgrep ignore with a link.
The point: a rule that always passes is noise; a rule that always fails gets disabled. Aim for ~80% pass on first commit, ~95% on second commit.
Dependency scanning
Two scanners are better than one — they disagree, and the disagreement is signal.
- OSV-Scanner for the SBOM-level view (covers OSS Index, GitHub Advisories, Go vuln DB, npm audit DB).
- Trivy filesystem scan for license and config issues OSV doesn't cover.
Block PRs on HIGH+ with a known fix version. Everything else goes to a weekly Renovate-driven batch. Don't block on issues you can't fix.
Container scanning
Scan the image you actually ship, not "what the Dockerfile probably builds". Run it after docker build in the same job. Cache the vulnerability DB between runs — first-pull is 30s, subsequent 3s.
Use a baseline file (.trivyignore) but require an expiry date on every ignore. CI fails when an ignore expires. The compromise: developers can mute findings, but only temporarily.
SBOM as a deliverable
Generate an SBOM (Syft, SPDX or CycloneDX) on every release build. Attach it to the OCI image as a referrer (Cosign attestation) or to the GitHub Release. You'll thank yourself the first time a Log4Shell-shaped event hits and you can answer "which builds shipped this?" in 30 seconds instead of three days.
DAST: don't run it in PRs
ZAP baseline scans take 3–10 minutes and produce mostly the same findings every time. Run them post-merge on main, with results uploaded to GitHub Code Scanning. Page on a new finding only — diff-driven, not absolute.
OIDC and the death of static keys
If a single thing on this list mattered: kill long-lived cloud credentials in CI. Use OIDC trust between GitHub Actions / GitLab and your cloud IAM. Configure the trust policy with branch and repo conditions, not just aud=sts.amazonaws.com.
{
"StringLike": {
"token.actions.githubusercontent.com:sub":
"repo:org/repo:ref:refs/heads/main"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
The measure of success
Three numbers I track for any pipeline I touch:
- Time-to-first-feedback on a PR — target under 3 minutes.
- Override rate — how often security gates are bypassed. Above 5% means the gate is broken, not the team.
- Time-to-fix on critical findings — should be hours, not weeks.
None of this is exotic. The hard part is having the discipline to remove rules that don't pay rent, and the credibility to add the ones that do.
If you'd like a hand wiring up a pipeline like this — get in touch.