The Problem: Containers as a Black Box
Every time you run docker build, you're not just building an application — you're assembling layer upon layer of operating system, libraries, and third-party packages. According to security reports from 2025–2026, over 80% of critical vulnerabilities in production environments come from third-party dependencies within images. Manual auditing of hundreds of images per day is impossible, and postponing checks until the penetration testing stage is already too late.
The solution is to integrate vulnerability scanning directly into the CI/CD pipeline. One of the most popular tools for this is Trivy by Aqua Security. It's lightweight, fast, and doesn't require external databases. In this article, I'll show you how to integrate Trivy into GitLab CI, configure threshold policies, and automatically block builds if critical vulnerabilities are found.
Why Trivy Instead of Clair or Anchore?
There are several container scanning tools on the market. Here's a brief comparison as of 2026:
| Tool | Type | Vulnerability Database | Scan Speed | GitLab CI Integration |
|---|---|---|---|---|
| Trivy | CLI scanner | NVD, Red Hat, Debian, Alpine, OSV | < 10 seconds per image | Native (Docker image) |
| Clair | Server scanner | NVD, Red Hat, Debian | ~30–60 seconds | Requires API |
| Anchore | CLI + server | NVD, proprietary | ~20–40 seconds | Docker image |
| Grype | CLI scanner | NVD, OSV | ~10–15 seconds | Docker image |
Trivy wins because it doesn't require deploying a separate server, works out of the box, and supports scanning not only images but also filesystems, Git repositories, and IaC templates (Terraform, Kubernetes). This makes it an ideal candidate for a DevSecOps pipeline.
Concept: Trivy + GitLab CI
Our pipeline will look like this:
- Build —
docker buildthe image. - Scan — Trivy checks the image for vulnerabilities.
- Analysis — if critical (CRITICAL) or high (HIGH) vulnerabilities with a CVSS score > 7.0 are found, the pipeline fails.
- Publish — if the image is secure, push it to the registry.
- Notification — send the report as a GitLab CI job artifact.
Config: Writing .gitlab-ci.yml
Let's get straight to it. Here's a minimal config that does exactly what's described above:
stages:
- build
- scan
- publish
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
TRIVY_VERSION: "0.56.0"
build:
stage: build
image: docker:27.0-cli
services:
- docker:27.0-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
only:
- main
trivy-scan:
stage: scan
image: docker:27.0-cli
services:
- docker:27.0-dind
variables:
DOCKER_HOST: tcp://docker:2375
before_script:
- docker pull aquasec/trivy:$TRIVY_VERSION
script:
- docker run --rm
-v /var/run/docker.sock:/var/run/docker.sock
-v $CI_PROJECT_DIR:/project
aquasec/trivy:$TRIVY_VERSION
image --exit-code 1 --severity CRITICAL,HIGH
--ignore-unfixed
$DOCKER_IMAGE
after_script:
- docker run --rm
-v /var/run/docker.sock:/var/run/docker.sock
-v $CI_PROJECT_DIR:/project
aquasec/trivy:$TRIVY_VERSION
image --format json --output /project/trivy-report.json
$DOCKER_IMAGE
- docker run --rm
-v /var/run/docker.sock:/var/run/docker.sock
-v $CI_PROJECT_DIR:/project
aquasec/trivy:$TRIVY_VERSION
image --format table
$DOCKER_IMAGE
artifacts:
paths:
- trivy-report.json
expire_in: 30 days
only:
- main
publish:
stage: publish
image: docker:27.0-cli
services:
- docker:27.0-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker tag $DOCKER_IMAGE $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
needs:
- trivy-scan
Key Points of the Config
--exit-code 1— forces Trivy to return a non-zero exit code if vulnerabilities of the specified severity are found. GitLab CI will treat this as a job failure.--severity CRITICAL,HIGH— we only block on critical and high vulnerabilities. Low and Medium can be logged but not blocked.--ignore-unfixed— ignore vulnerabilities for which no patch is yet available. This is important: if you block a build due to an unfixable vulnerability, you'll be stuck in an infinite loop.artifacts— save the JSON report for later analysis.
Code: Writing a Custom Script for Flexible Policy
The basic config is good for a start, but in real production you'll need more flexible logic. For example, blocking only vulnerabilities with CVSS >= 7.5 or allowing exceptions for known CVEs.
Let's create a scan-policy.sh script:
#!/bin/bash
set -euo pipefail
IMAGE=$1
THRESHOLD=${2:-7.0} # CVSS threshold
# Scan and save JSON
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:0.56.0 \
image --format json --quiet "$IMAGE" > report.json
# Parse with jq (must be installed)
HIGH_COUNT=$(cat report.json
| jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH") | select(.CVSSScore // 0 >= '$THRESHOLD')] | length')
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "❌ Found $HIGH_COUNT vulnerabilities with CVSS >= $THRESHOLD"
cat report.json
| jq '.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH") | select(.CVSSScore // 0 >= '$THRESHOLD') | {CVE: .VulnerabilityID, Severity: .Severity, CVSS: .CVSSScore, PkgName: .PkgName}'
exit 1
else
echo "✅ Image is secure. No critical vulnerabilities above threshold $THRESHOLD found."
exit 0
fi
In GitLab CI, you can call it like this:
trivy-advanced:
stage: scan
image: docker:27.0-cli
services:
- docker:27.0-dind
before_script:
- apk add --no-cache jq
- docker pull aquasec/trivy:$TRIVY_VERSION
script:
- chmod +x scan-policy.sh
- ./scan-policy.sh $DOCKER_IMAGE 7.5
artifacts:
reports:
json: report.json
Production: What to Add for Real-World Use
In a production environment, a single scan is not enough. Here's what else you should implement:
1. Scanning at the Development Stage (pre-commit)
Add Trivy to pre-commit hooks or a local docker-compose so developers see issues before pushing. Example Makefile target:
scan-local:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:0.56.0 image my-app:latest
2. Integration with a Vulnerability Management System
Trivy can export reports in SARIF, CycloneDX, and SPDX formats. SARIF can be uploaded to GitLab for display in MRs:
trivy-sarif:
stage: scan
script:
- docker run --rm ... aquasec/trivy ... --format sarif --output gl-code-quality-report.sarif
artifacts:
reports:
codequality: gl-code-quality-report.sarif
3. Allowlist for Exceptions
Sometimes a vulnerability exists but cannot be fixed (e.g., the system kernel). Create a .trivyignore file:
CVE-2023-1234
CVE-2024-5678
And pass it to the container:
script:
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $CI_PROJECT_DIR/.trivyignore:/root/.trivyignore aquasec/trivy ... --ignorefile /root/.trivyignore
4. Monitoring and Alerts
Even if you block builds, you need to know what vulnerabilities exist in already running containers. Use the Trivy Operator (for Kubernetes) or a cron job that scans the registry daily and sends a report to Slack/Telegram. ASI Biont supports Telegram connectivity via API — more details at asibiont.com
Conclusion: DevSecOps Isn't Scary
Automating container security scanning with Trivy and GitLab CI is one of the simplest and most effective steps toward implementing DevSecOps. You get:
- Automatic detection of vulnerabilities at the build stage.
- Blocking of dangerous images before they reach production.
- Transparency through reports in CI artifacts.
- Scalability — Trivy works even on large monorepos.
Setup takes 15 minutes and saves hours of manual auditing while preventing incidents. If you want to dive deeper into building production-ready pipelines, including canary deployments, secrets management, and GitOps approaches — the asibiont.com platform offers a full CI/CD and GitOps course that covers these scenarios with real configs.
Start small — add Trivy to your next merge request. Security should be automated, not postponed.
Comments