CI/CD Security Gates: Which Findings Should Block a Deploy

# CI/CD Security Gates: Which Findings Should Block a Deploy
Block a deployment only on findings that are both serious and reachable: a known-exploited vulnerability (on CISA's KEV list) or a high exploitation probability (EPSS) in code your application actually loads, plus any verified live secret. Everything else scans, records, and files a ticket, but it does not stop the pipeline. Well-designed CI/CD security gates enforce that small blocking set and leave the long tail as report-only, so you stop exploitable code without turning delivery into a queue.
That is the whole argument. The rest of this article is how to implement it: how to split blocking from report-only findings, why raw CVSS is the wrong trigger, where each scanner belongs, how to keep scan time down, and what a person should do when a gate goes red.
Blocking findings versus report-only findings
The failure mode in most pipelines is that a scanner is wired to fail the build on any "high" or "critical" finding. Within a week the team is drowning, someone adds a blanket ignore, and the gate now protects nothing. The fix is to decide, deliberately, which findings are allowed to break a build.
A finding should block only when three things are true at once: the vulnerability is exploitable in the wild or highly likely to be, the vulnerable code path is reachable from your application, and there is an action the author of this change can take now. If any of those is false, the finding is real but it is not a deploy-stopper. It belongs in a tracked backlog with an owner and a due date, not in the path of a release.
Secrets are the exception that proves the rule. A verified live credential in a diff is always blocking, because the remediation is unambiguous (rotate and remove) and the blast radius is immediate. There is no "report-only" for a working AWS key in a commit.
| Signal | Block the deploy | Warn (non-blocking) | Report only |
|---|---|---|---|
| Dependency CVE on CISA KEV | Yes | - | - |
| Dependency CVE, high EPSS, reachable | Yes | - | - |
| High CVSS, not reachable, low EPSS | - | Optional | Yes |
| New finding introduced by this change | - | Yes | - |
| Pre-existing finding, no fix available | - | - | Yes |
| Verified live secret in diff | Yes | - | - |
| SAST high-confidence injection in changed code | Yes | - | - |
| SAST low-confidence / style finding | - | - | Yes |
The column that matters most is "new finding introduced by this change." Gating on the delta rather than the total is the single most effective way to keep a gate credible: authors are accountable for what they add, and the historical backlog is handled on its own schedule instead of blocking an unrelated feature.
Prioritise by exploitability and reachability, not raw CVSS
CVSS tells you how bad an exploit would be if it happened. It says nothing about whether anyone is exploiting it or whether your code even calls the vulnerable function. Gating on CVSS alone is why "critical" fatigue sets in. Two better signals exist, and they are both public.
Known exploitation and exploitation probability
CISA maintains the Known Exploited Vulnerabilities (KEV) catalogue, and it is worth building around. CISA describes it as the authoritative source of vulnerabilities that have been exploited in the wild, and advises organisations to use the KEV catalogue as an input to their vulnerability management prioritisation framework. The NVD's announcement of the catalogue carries the same guidance, noting that CISA strongly recommends all organisations review and monitor the KEV catalogue and prioritise remediation of the listed vulnerabilities to reduce the likelihood of compromise by known threat actors. If a CVE in your dependency tree is on KEV, that is your highest-confidence block signal. It has moved from theoretical to observed.
For everything not yet on KEV, use the Exploit Prediction Scoring System. EPSS predicts the probability that a vulnerability will be exploited in the wild within the next 30 days, on a scale from 0 to 1, where higher scores mean higher risk. It also publishes a percentile so you can see how a given CVE ranks against every other. The practical move is to pick a threshold (we start around the top few percent) and treat a high EPSS score as a block candidate. GitHub now surfaces this directly: Dependabot includes EPSS scores to help teams focus on the alerts most likely to be exploited, and its guidance is to prioritise using severity (CVSS), exploitation likelihood (EPSS), and repository properties together rather than any one number.
One honest caveat: KEV criteria and the directives that reference it evolve. CISA has issued Binding Operational Directive 26-04, a risk-based directive that supersedes and revokes BOD 22-01 and requires federal civilian agencies to prioritise remediation across several criteria, one of which is whether a vulnerability appears on the KEV catalogue. Those directives are binding on US federal civilian agencies, not on you, but the catalogue and its methodology are a free, high-quality input for any prioritisation policy.
Reachability
Reachability is the difference between "this CVE is in a package in my lockfile" and "my application actually executes the vulnerable code." A vulnerability in a transitive dependency that you import but never call lives in code your application does not execute, though reachability alone is not a guarantee. Reachability analysis (call-graph based) can help reduce the blocking set, because many CVEs in a large dependency tree are never reached.
Treat reachability as a downgrade signal, not an upgrade signal. Use it to move a high-CVSS-but-unreachable finding from blocking to report-only. Do not use "we could not prove reachability" as an excuse to block everything, and do not use "the tool says unreachable" to auto-close a KEV entry. When exploitation is confirmed in the wild, patch regardless of your call graph.
Where CI/CD security gates live in the pipeline
Put each scanner where it can give the fastest, cheapest true signal, and gate at the point where a human can still act without a context switch.
- Secrets scan: pre-commit and on push. The earliest possible point. Blocking a secret before it reaches the remote is worth more than detecting it after. Run a fast secrets scanner as a pre-commit hook and again as a required push/PR check so nothing slips through a bypassed hook.
- Static analysis (SAST): on pull request, scoped to changed code. SAST belongs on the diff. High-confidence findings in newly changed lines (injection, deserialisation, path traversal) are strong block candidates. Whole-repository SAST runs on a schedule, not on every PR, because it is slow and its findings are mostly pre-existing.
- Dependency / SCA scan: on pull request and on the lockfile. This is where KEV, EPSS, and reachability come together. Gate on new vulnerable dependencies introduced by the change; report on the standing backlog.
- Container / image scan: at build, before push to registry. Scan the built image for OS and library CVEs. Base-image vulnerabilities dominate here, so the highest-leverage fix is usually a smaller or more current base image, not a per-CVE chase.
A minimal policy, expressed as data rather than scattered across scanner flags, keeps the rules auditable:
# security-gate.yml — one place that decides what blocks
block_if:
- finding.type == "secret" and finding.verified == true
- finding.type == "dependency" and finding.on_kev == true
- finding.type == "dependency" and finding.epss >= 0.5 and finding.reachable == true
- finding.type == "sast" and finding.confidence == "high" and finding.introduced_by_pr == true
warn_if:
- finding.introduced_by_pr == true # surface the delta, do not block
report_only:
- true # everything else is a ticket
Wire the evaluator into the job's exit code so the CI system fails the stage only when block_if matches:
# fail the build only on blocking findings; always publish the full report
scan --format sarif --output findings.sarif
gate eval --policy security-gate.yml --input findings.sarif
status=$?
publish-report findings.sarif # dashboard/PR annotation, runs regardless
exit $status # non-zero only if a block rule fired
Building and maintaining that shared evaluator, so every team gets the same rules instead of copy-pasted scanner config, is exactly the kind of paved-road work we do in Cybersecurity and DevSecOps, and it fits alongside the pipeline tooling covered by our Platform and DevOps practice.
Keep scan time low with caching and changed-path scoping
A gate that adds ten minutes to every PR will be routed around. Two techniques help keep the added latency low.
Changed-path scoping. Do not rescan the world on every commit. Run SAST only over files touched in the diff, run SCA only when a manifest or lockfile changed, and run the image scan only when the Dockerfile or its inputs changed. If a PR only edits documentation, no security stage needs to run at all.
Caching. Vulnerability databases, dependency graphs, and downloaded advisories are expensive to fetch and rarely change within a day. Cache the scanner's vulnerability DB and your resolved dependency tree keyed on the lockfile hash, so a rebuild with unchanged dependencies reuses the previous result. For container scans, structure your image so unchanged base layers hit the layer cache and only the application layer is re-examined.
Fail fast, and put the slow full scan off the critical path. Keep the PR gate lean. Run the exhaustive nightly scan on the main branch, feed its output into the same policy, and open tickets automatically. The PR sees only the delta; the backlog is managed asynchronously.
What to do when a gate fires
A red gate is a prompt, not a verdict. The author needs three things on screen immediately: which finding blocked, why it is considered blocking (KEV, EPSS, reachable, verified secret), and the shortest path to green.
For a secret, the runbook is fixed: treat the credential as compromised, rotate it at the source, remove it from history, and move it into your secrets manager. Do not just delete the line; a pushed secret is already exposed.
For a dependency, the first option is to bump to a fixed version, ideally automated. If no fix exists yet, that is the one legitimate case for a time-boxed, reviewed exception: an expiring waiver, attached to a ticket with an owner, that a security reviewer signs off. An exception that never expires is just a silent ignore with extra steps.
For a SAST finding, either fix the code or, if it is a genuine false positive, suppress it inline with a justification that shows up in review. The suppression itself is part of the diff, so it gets the same scrutiny as the code.
Design the exception path as carefully as the gate. If getting a waiver is harder than fixing the code, people fix the code, which is what you want. If waivers are frictionless and unbounded, the gate erodes. The whole system only works if firing a gate is rare, legible, and quick to resolve, which is the same discipline we bring to on-call and operability in our Reliability and SRE work.
Frequently asked questions
Should a high CVSS score block a deployment on its own?
No. CVSS measures potential impact, not whether a vulnerability is being exploited or whether your code reaches it. Use it as one input, but gate on exploitation signals (KEV and EPSS) combined with reachability. A CVSS 9.8 in a package you never call is a report-only ticket, while a moderate-CVSS CVE on the KEV list is a block.
What EPSS threshold should we use to block a build?
There is no universal number, and it depends on how much risk you carry and how much noise you can absorb. Because EPSS is a probability from 0 to 1 with a published percentile, a reasonable starting point is to block on scores in the top few percent and tune from there. Review the threshold quarterly against how many builds it actually stopped and whether those were real.
Where should secret scanning run, in the pipeline or before it?
Both. Run a secrets scanner as a pre-commit hook so developers catch keys before they leave the laptop, and run it again as a required check on push or pull request so a bypassed hook does not defeat the control. A verified live secret should always block, and the response is to rotate the credential, not just delete the line.
How do we stop security scans from slowing every pull request?
Scope and cache. Only scan what changed (SAST on the diff, SCA when the lockfile moves, image scans when the Dockerfile moves), and cache the vulnerability database and resolved dependency graph keyed on the lockfile hash. Keep the exhaustive full scan on a nightly main-branch job that feeds the same policy, so the PR path stays fast.
What is the difference between a blocking gate and report-only scanning?
A blocking gate fails the build and prevents the deploy; report-only scanning records the finding, annotates the pull request, and files a ticket without stopping anything. The point of separating them is to reserve blocking for findings that are exploitable, reachable, and actionable now, so the gate stays credible. Everything else is tracked and fixed on a schedule.