Back to posts
Post

Runtime CVE Scanning with Trivy in GitLab CI: Auto-block Images

Integrate Trivy into GitLab CI to scan container images at runtime and automatically block vulnerable builds before deployment.

DevopsTrivyGitLab CIcontainer securityCVE scanningDevSecOps

Runtime vulnerability scanning is non-negotiable for production workloads. I’ve seen too many teams rely solely on build-time checks, only to get hit by zero-days or misconfigured base images that slip through. In my environment, Trivy has become the go-to tool for catching CVEs in container images — not just during build, but at the point where risk matters most: right before deployment. Integrating it into GitLab CI gives you automated gates that stop dangerous images from reaching staging or prod.

Why Runtime Scanning Matters More Than Ever

Build-time scanning catches known issues in your Dockerfile and base layers, but it doesn’t account for what happens when the image runs. New CVEs emerge daily, and if your base image pulls in a vulnerable package after your CI job finishes, you’re exposed. Runtime scanning evaluates the image as it would exist in a running container — including transient layers, ephemeral files, and dynamic dependencies. This is especially critical for images built from scratch or using minimal bases like distroless or Alpine, where package managers aren’t always obvious.

I use Trivy because it’s fast, lightweight, and supports both vulnerability and misconfiguration scanning. More importantly, it integrates cleanly into GitLab CI without needing extra agents or complex setup. The goal isn’t just to scan — it’s to fail the pipeline when critical or high-severity CVEs are found.

Setting Up Trivy in GitLab CI

First, add Trivy to your .gitlab-ci.yml. I use the official Aqua Security image to keep things consistent and avoid version drift.

trivy-scan:
  image: aquasec/trivy:latest
  stage: test
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'

This job pulls the Trivy scanner, runs it against the image built earlier in the pipeline, and exits with code 1 if any HIGH or CRITICAL vulnerability is found. The --exit-code 1 flag is key — it turns Trivy’s output into a hard failure that GitLab CI respects.

I place this after the build and test stages but before any deployment. That way, if the image fails the scan, nothing gets promoted. You can also add a needs: clause to ensure it runs only after the image is built and pushed to the registry.

Handling False Positives and Baselines

Not every flagged CVE is actionable. Some are in packages not actually used by your application, or are only exploitable under specific conditions you don’t expose. Trivy lets you create a .trivyignore file to suppress known false positives.

I generate a baseline scan on a trusted image and save the ignored vulnerabilities:

trivy image --ignore-unfixed --format template --template "@contrib/gitlab.tmpl" -o trivy-baseline.json myapp:stable

Then, in CI, I compare new scans against this baseline to avoid noise. This keeps the signal clean without disabling scanning entirely.

Automating Image Blocking at the Registry Level

Scanning in CI is great, but what if someone pushes an image manually? To catch those cases, I extend the protection to the registry itself using GitLab’s Container Registry security policies.

Go to your project’s Settings > Security & Compliance > Container scanning and enable the Trivy integration. Set the failure threshold to "High" or "Critical". Now, even if a developer pushes an image directly via docker push, GitLab will scan it on push and block further promotion if it fails the policy.

This dual-layer approach — CI gate + registry enforcement — gives me confidence that no vulnerable image slips through, whether it comes from a pipeline or a manual push.

Tip: Cache Trivy DB to Speed Up Scans

Trivy downloads its vulnerability database on each run, which adds latency. In CI, I cache it between jobs to cut scan time by 60% or more.

trivy-scan:
  image: aquasec/trivy:latest
  stage: test
  cache:
    key: "trivy-db"
    paths:
      - /home/trivy/.cache/trivy/db
  script:
    - trivy image --download-db-only
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

The first run downloads the DB and caches it; subsequent jobs reuse it. Just make sure your runner has enough disk space — the DB is ~200MB and grows over time.

Warning: Don’t Rely on Scanner Alone

Trivy is excellent, but it’s not a silver bullet. It won’t catch logic flaws, hardcoded secrets in volumes, or misconfigured runtime flags. I pair it with runtime admission controllers like OPA/Gatekeeper in Kubernetes and regular image rebasing to keep base layers fresh. Think of Trivy as your first line of defense — essential, but part of a larger strategy.

As I mentioned before in my post on SBOM and license checks (https://furkanikkan.com/urun/gitlab-ci-de-trivy-ile-otomatik-sbom-ve-lisans-uyumluluk-kontrolu-138), Trivy’s strength is in automation and policy enforcement. When you make scanning a non-optional step in your CI flow, you shift security left without slowing teams down.

Final Thoughts

Runtime CVE scanning with Trivy in GitLab CI isn’t about perfection — it’s about reducing risk systematically. By blocking images with high-severity vulnerabilities before they leave your CI system, you prevent known exploits from reaching production. Combine that with registry-level enforcement and smart caching, and you get a pipeline that’s both secure and efficient.

If you’re not scanning images at the point of deployment risk, you’re flying blind. Start with Trivy in CI, set hard failure thresholds, and enforce it at the registry. Your future self — and your incident response team — will thank you.


Cover image: Unknown · CC0 (Openverse / kamu malı) · https://www.rawpixel.com/image/6042704/photo-image-public-domain-free