CI Security Gate Guide

Your build passed. The scanner found issues.

Most teams add a security scanner to CI and call it done. Then a critical vulnerability ships to production because the scanner ran in report-only mode with exit code 0. The build passed. The finding was a warning. Nobody noticed. This guide covers how to close that gap — from exit-code gating to posture-score thresholds.

Monitoring vs enforcement: the two-step mistake

Monitoring only — silently passes
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
scan-type: 'image'
image-ref: 'myapp:latest'
severity: 'CRITICAL,HIGH'
exit-code: '0' # ← never fails
Enforcement — blocks the build
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
scan-type: 'image'
image-ref: 'myapp:latest'
severity: 'CRITICAL,HIGH'
exit-code: '1' # ← fails on findings

Choosing a severity threshold that teams actually respect

SeverityActionRationale
CRITICALBlock merge immediatelyExploitable, public PoC exists or EPSS > 0.5; ship = compromise
HIGHBlock merge (once baseline is clean)Significant exploitability; block after resolving existing inventory
MEDIUMLog + ticket; do not blockOften theoretical; blocking creates noise that trains bypass habits
LOW / UNKNOWNLog onlyTrack for awareness; no pipeline impact

Where CVE gates fall short

The posture-score gate: a threshold the whole box must pass

GitHub Actions — Keelix posture gate
- name: Keelix security gate uses: jakelamon/keelix@v0.1.0 with: fail-on: critical # block on any CRITICAL finding min-score: 70 # block if posture score < 70 # score covers host config, containers, exposure + AI/MCP

One class of finding image-only gates will never catch

Keelix scan findings — example
CRITMCP server running with auto-approval enabled — all tool calls execute without confirmation
CRITPlaintext API key found in .mcp.json — rotatable secret stored unencrypted on disk
WARNContainer running as root (UID 0) — no user namespace remapping configured
PASSDocker daemon API not exposed on TCP — Unix socket only
None of these findings appear in any CVE database.

A recommended two-gate pipeline

01

Image CVE gate (PR / pre-merge)

Run a CVE scanner (Trivy, Grype) with exit-code: 1 on CRITICAL severity. Fast feedback to the developer — catches known vulnerabilities in packages before they land on main. Add ignore-unfixed: true to avoid noise from unresolvable findings.

02

Posture gate (pre-deploy / pre-push to registry)

Run keelix scan with a minimum-score threshold on the target environment or a staging replica. This gate catches the configuration and exposure findings that image scanners miss: exposed ports, privileged containers, MCP auto-approval, plaintext secrets in agent configs.

03

Set the threshold incrementally

Run keelix scan on your server today and record the baseline score. Set min-score 5-10 points below that baseline as the initial gate. Fix a batch of findings, raise the threshold, repeat. A cliff-edge gate on day one causes bypasses; a ratchet builds genuine improvement.

04

Track medium/low separately

Pipe medium and low findings to a ticket tracker or Keelix Cloud logbook. Do not block the build on them. The goal is visibility without friction — medium findings you cannot fix today should be tracked, not silently dropped and not used as a blocking condition that trains engineers to suppress alerts.

Quick reference: exit codes

Gate on the whole box, not just the image.

Free, local-first, Apache-2.0. Run keelix scan today to get your baseline score — then set the threshold and let CI enforce it.

operator@host — keelix
# install — free & open source (Apache-2.0)
$ curl -fsSL https://keelix.dev/install.sh | sh
$ keelix scan