How to Automate Container Image Security Scanning with Trivy and GitLab CI: A Step-by-Step Guide

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:

  1. Builddocker build the image.
  2. Scan — Trivy checks the image for vulnerabilities.
  3. Analysis — if critical (CRITICAL) or high (HIGH) vulnerabilities with a CVSS score > 7.0 are found, the pipeline fails.
  4. Publish — if the image is secure, push it to the registry.
  5. 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.

← All posts

Comments