Integrating Trivy Image Security Scanning into GitLab CI/CD Pipelines
This tutorial demonstrates how to integrate Trivy image security scanning into a GitLab CI/CD pipeline, covering tool selection, Dockerfile creation, pipeline configuration, scheduled scans, handling vulnerability reports, and strategies for failing builds based on severity levels.
Using GitLab CI and Trivy
Introduction
Image security scanning has become increasingly popular. The idea is to analyze a Docker image and look for vulnerabilities based on a CVE database, allowing you to know which vulnerabilities are present before using the image in production.
There are several ways to scan Docker images depending on the tool you use. You can run a scan from the CLI, integrate it directly into a container registry, or, preferably, embed the scan into a CI/CD pipeline. The latter automates the process and continuously analyzes generated images, aligning with DevOps principles.
Below is a simple example:
Today I will show you how to set up image security scanning integrated into a CI/CD pipeline.
Tools
Several tools can perform image security scanning:
Trivy – developed by Aqua Security.
Anchore – developed by Anchore Inc.
Clair – developed by Quay.
Docker Trusted Registry – built‑in scanning for Docker Enterprise.
Cloud providers (Azure/AWS/GCP) – often offer scanning as a service.
For this tutorial we will use Trivy on a GitLab CI pipeline.
Trivy Quick Overview
Trivy is an easy‑to‑use yet accurate image scanner. Installation is straightforward:
$ curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/install.sh | sh -s --b /usr/local/bin
$ sudo mv ./bin/trivy /usr/local/bin/trivy
$ trivy --versionBasic usage:
$ trivy image nginx:alpineThe command produces output similar to the screenshot below:
Adding a Simple Docker Image
We need a Docker image to demonstrate the scan. Here is a minimal Dockerfile:
FROM debian:buster
RUN apt-get update && apt-get install nginx -yBuild the image locally with:
$ docker build -t security_scan_example:latest .Push the Dockerfile to a GitLab project.
Creating a Simple CI/CD Pipeline
We will use GitLab CI. First, add a build job:
build:
stage: build
image: docker:stable
services:
- docker:dind
tags:
- docker
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:latestThe job runs on a docker:stable container, builds the image from the Dockerfile, and pushes it to the GitLab container registry.
Next, add the security scan job:
security_scan:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
services:
- docker:dind
tags:
- docker
script:
- trivy --no-progress --output scanning-report.txt $CI_REGISTRY_IMAGE:latest
artifacts:
reports:
container_scanning: scanning-report.txtThis job runs Trivy inside the official Trivy image, scans the built image, and stores the report as a pipeline artifact.
After pushing code, both jobs run automatically, as shown in the pipeline view screenshots below:
The security scan job produces a report with 114 low, 8 medium, 24 high, and 1 critical vulnerability.
Where is the Report?
The report is saved as an artifact and can be downloaded from the job details page. After downloading, you can view detailed information such as affected libraries, CVE IDs, severity, and possible fixes.
What to Do Next?
By default Trivy exits with code 0, so the pipeline never fails even if vulnerabilities are found. To fail the pipeline on critical issues, use Trivy’s --severity and --exit-code options.
script:
- trivy --no-progress --output scanning-report.json $CI_REGISTRY_IMAGE:latest
- trivy --exit-code 1 --no-progress --severity CRITICAL $CI_REGISTRY_IMAGE:latestNow the job will succeed only if no critical vulnerabilities are detected.
Final Step…
So far the scan runs only when the image is built/pushed. Because new CVEs appear daily, we add a scheduled pipeline (e.g., every night at 2 AM) that runs only the security_scan job.
Create a variable SCHEDULED_PIPELINE with value security_scan in the schedule, then adjust the .gitlab-ci.yml to use a template and conditional execution:
.scanning-template: &scanning-template
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
services:
- docker:dind
tags:
- docker
script:
- trivy --no-progress --output scanning-report.json $CI_REGISTRY_IMAGE:latest
- trivy --exit-code 1 --no-progress --severity CRITICAL $CI_REGISTRY_IMAGE:latest
artifacts:
reports:
container_scanning: scanning-report.json
build:
stage: build
image: docker:stable
services:
- docker:dind
tags:
- docker
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:latest
except:
variables:
- $SCHEDULED_PIPELINE
security_scan:
<<: *scanning-template
except:
variables:
- $SCHEDULED_PIPELINE
security_scan:on-schedule:
<<: *scanning-template
only:
variables:
- $SCHEDULED_PIPELINE == "security_scan"Now normal pushes trigger both build and scan, while the scheduled pipeline runs only the scan at the defined time.
How to Fix These Vulnerabilities?
Typically you upgrade the base image or the vulnerable packages (e.g., upgrade nginx). Another approach is to minimize the image by removing unnecessary components.
For example, switch to an Alpine base image:
FROM alpine:3.12
RUN apk update && apk add nginx -yAfter updating the Dockerfile, the pipeline runs successfully with zero reported vulnerabilities.
Conclusion
We have shown how easy it is to integrate a security scanning job into a GitLab CI pipeline using Trivy. In real‑world projects with multiple branches, additional configuration may be required, but the core concept remains the same.
DevOps Cloud Academy
Exploring industry DevOps practices and technical expertise.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.