"Block criticals" is not a policy — it is a sentence. A real vulnerability policy gate answers harder questions: Does a high-severity finding block a release if it is not reachable? What about a license violation? How long can a team defer a fix, and who signs off? Without written, enforceable answers, every finding becomes an ad-hoc argument in a pull request, and the loudest engineer wins. This guide shows how to encode a vulnerability policy as version-controlled configuration, enforce it identically everywhere, and manage exceptions so they expire instead of accumulating.
Prerequisites
- A repository already being scanned by the Safeguard CLI (
sg scanruns and returns findings). - Agreement from your team on a baseline severity threshold — you can tighten it later.
- The Safeguard CLI installed and authenticated with a project token.
Step 1: Define the policy as code
Create .safeguard/policy.yaml. This is the single source of truth every tool reads:
name: default-strict
rules:
# Block by severity, but only when reachable
- fail_on: critical
condition: always
- fail_on: high
condition: reachable
# License policy
- deny_licenses: [GPL-3.0, AGPL-3.0]
applies_to: production
# No secrets, ever
- fail_on_secret: true
exceptions:
require_expiry: true
require_reviewer: true
max_age_days: 90
This says: criticals always block; highs block only if reachable; copyleft licenses are denied in production; secrets always block; and every exception must have an owner and an expiry no more than 90 days out.
Step 2: Point your config at the policy
Reference the policy file in .safeguard/config.yaml so scans use it by default:
project: checkout-service
policy: .safeguard/policy.yaml
Commit both files. From now on the policy lives in code review, and changing what blocks a release requires a reviewed pull request.
Step 3: Test the gate locally
Dry-run the policy against current findings before you enforce it:
sg policy test
You will see exactly what would pass and fail:
Evaluating policy: default-strict
critical: 1 → BLOCK (always)
high: 6 → 2 BLOCK (reachable), 4 pass
medium: 18 → pass
licenses: 0 violations
secrets: 0 findings
Result: FAIL (3 blocking findings)
Step 4: Enforce the gate in CI
Wire the same policy into your pipeline so it blocks merges:
- uses: safeguard-sh/gate@v2
with:
token: ${{ secrets.SAFEGUARD_TOKEN }}
policy: .safeguard/policy.yaml
Because CI reads the same file you tested locally, the pipeline result is never a surprise.
Step 5: Add a time-boxed exception
When a fix genuinely cannot land yet, record a reviewed, expiring exception rather than lowering the threshold for everyone:
# .safeguard/ignore.yaml
rules:
- id: CVE-2024-37891
package: urllib3
reason: "Internal-only service, no TLS downgrade path — SEC-1533"
expires: "2026-09-15"
reviewer: "daniel@example.com"
The gate flags the exception a week before it expires and fails the scan the moment it does, so "we will deal with it later" cannot quietly become "never."
Verify the result
Confirm the gate behaves as written:
# Policy evaluates and reports blocking findings
sg policy test --format json | jq '.result'
# Exceptions are valid, owned, and unexpired
sg policy validate-exceptions
If validate-exceptions fails, an entry is missing a reviewer or expiry, or has already expired — the gate refuses malformed exceptions by design.
How Safeguard streamlines this
A policy is only as good as its consistency. Safeguard's CLI evaluates the same policy.yaml locally, in CI, and at deploy time, so a change never passes one gate and fails another for reasons no one can explain. Reachability lets you write policies that block on exploitability rather than raw severity, which is what keeps a strict gate from becoming a wall of false blocks that teams route around. Because Secure Containers reads the same policy, your image gate and your source gate enforce identical rules — no separate policy language for runtime. Every exception is version-controlled, owned, and expiring, giving auditors a clean trail of who accepted which risk and until when. If you are comparing platforms, the comparison hub shows how a shared-policy model differs from per-tool configuration. See plans on the pricing page.
Ready to codify your gate? Connect a repository at app.safeguard.sh/register.
Frequently Asked Questions
What is a vulnerability policy gate? It is an automated checkpoint that evaluates a build's findings against written rules and blocks the release when the rules are violated. It replaces case-by-case human judgment in pull requests with a consistent, reviewable standard, so the same risk gets the same decision every time.
Should the gate block on severity or on reachability? Ideally both, layered. Block unconditionally on the most severe findings, and block on lower severities only when the vulnerable code is actually reachable. That combination catches the issues that matter while sparing your team from noise on vulnerabilities their code never touches.
How do I handle a vulnerability we cannot fix right now? Record a time-boxed exception with a reason, an owner, and an expiry date, kept in version control and code-reviewed. This documents the accepted risk explicitly and forces a re-decision when the exception lapses, rather than silently lowering the bar for the whole project.
Where should the policy live? In the repository, as version-controlled configuration read by every tool. Storing it as code means changes go through review, the same rules apply locally and in CI, and auditors can see the exact policy that was in force at any commit.
How is a policy gate different from just failing on criticals? Failing on criticals is a single hardcoded rule; a policy gate is a composable set of rules covering severity, reachability, licenses, and secrets, plus governed exceptions. The gate expresses your organization's actual risk tolerance, which is almost never captured by one severity threshold alone.
Explore the Safeguard CLI and Secure Containers, compare approaches on the comparison hub, or review plans on the pricing page. Full policy reference lives in the Safeguard docs.