🚀 Executive Summary
TL;DR: Manual Docker image security scanning is a significant time sink and risk, often missing critical vulnerabilities before production. This guide outlines how to automate Docker image scanning directly within GitLab CI pipelines using Trivy, ensuring security checks are integrated early and efficiently into the development workflow.
🎯 Key Takeaways
- Implement a structured GitLab CI pipeline with distinct `build`, `scan`, and `deploy` stages to ensure security scanning is a mandatory gate before deployment.
- Integrate Aqua Security’s Trivy into the `scan` stage, configuring it to scan images from the GitLab Container Registry, fail the pipeline on `CRITICAL` or `HIGH` severity vulnerabilities, and output `gl-container-scanning-report.json` for GitLab’s Security Dashboard.
- Utilize the `needs` keyword to enforce job dependencies (e.g., scan job runs only after build job completes) and implement caching for Trivy’s vulnerability database (`/root/.cache/trivy`) to significantly reduce scan times in subsequent pipeline runs.
Automate Docker Image Scanning in GitLab CI pipelines
Hey there, Darian Vance here. As a Senior DevOps Engineer at TechResolve, I’ve seen my fair share of last-minute scrambles before a release. One of the biggest time sinks used to be manual security reviews. I’d build an image, run a scanner locally, and then spend ages parsing logs. I probably wasted a good couple of hours a week on this until I realized the obvious: this entire process is a perfect candidate for automation. Integrating this directly into our GitLab CI pipeline was a game-changer. It’s not just about saving time; it’s about catching critical vulnerabilities before they ever get a chance to see a production environment. Let’s walk through how you can set this up.
Prerequisites
Before we dive in, make sure you have the following ready to go. This guide assumes you’re already comfortable with the basics.
- A GitLab project with a repository containing your application code and a
Dockerfile. - A GitLab Runner configured for your project, preferably with the Docker executor. This is key for building and scanning container images.
- The GitLab Container Registry enabled for your project.
- Familiarity with
.gitlab-ci.ymlsyntax.
The Step-by-Step Guide
Step 1: Structuring Your .gitlab-ci.yml
The first thing we need to do is define the stages for our pipeline. A logical flow is to build the image first, then scan it, and only if the scan passes, proceed to deployment. In my production setups, I always separate these concerns.
Let’s define our stages at the top of your .gitlab-ci.yml file:
stages:
- build
- scan
- deploy
Simple, right? This creates a clear, sequential workflow.
Step 2: The Build Job
Next, we create a job to build our Docker image and push it to the GitLab Container Registry. This image will be the target for our scanner in the next stage. We’ll tag the image with the commit SHA to ensure we’re always scanning the exact version of the code that triggered the pipeline.
build-image:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
What’s happening here? We’re using the official Docker image (`docker:20.10.16`) and the Docker-in-Docker (`dind`) service to create a clean environment for building our image. The script logs into GitLab’s registry using predefined CI/CD variables, builds the image, and pushes it up. That `IMAGE_TAG` variable is crucial for the next step.
Step 3: The Scanning Job with Trivy
Now for the main event. We’ll add a job that pulls the image we just built and scans it for vulnerabilities. I’m a big fan of Aqua Security’s Trivy—it’s fast, simple to use, and integrates beautifully with GitLab.
This job will run in the `scan` stage and will use the official Trivy image.
container-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
IMAGE_TO_SCAN: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
TRIVY_USERNAME: $CI_REGISTRY_USER
TRIVY_PASSWORD: $CI_REGISTRY_PASSWORD
TRIVY_AUTH_URL: $CI_REGISTRY
GIT_STRATEGY: none # We don't need the source code for this job
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH --output gl-container-scanning-report.json $IMAGE_TO_SCAN
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
needs:
- build-image
Let’s break down the important parts of this `script` command:
trivy image ... $IMAGE_TO_SCAN: This tells Trivy to scan the Docker image we tagged earlier.--exit-code 1: This is the most important flag. If Trivy finds any vulnerabilities matching the severity level, it will exit with a non-zero code, which automatically fails the GitLab CI job and stops the pipeline.--severity CRITICAL,HIGH: We’re telling Trivy to only fail the pipeline for `CRITICAL` or `HIGH` severity vulnerabilities. You can adjust this to include `MEDIUM` or `LOW`, but I find this is a good starting point to avoid noise.--output gl-container-scanning-report.json: This formats the output into a special JSON file named `gl-container-scanning-report.json`. GitLab automatically recognizes this filename and uses it to populate the Security Dashboard and Merge Request widgets. It’s a fantastic piece of integration.
The `artifacts` section is what makes the magic happen. By declaring the report as a `container_scanning` artifact, you’ll see a full, interactive security report right within the GitLab UI.
Pro Tip: When you’re first setting this up, a failing pipeline can be disruptive. Consider adding
allow_failure: trueto thecontainer-scanjob. This will run the scan and generate the report, but it won’t block your merge requests or deployments. Once your team gets into the habit of fixing vulnerabilities and you’ve fine-tuned the severity levels, you can remove it to enforce the security gate.
Common Pitfalls (And How to Avoid Them)
I’ve set this up dozens of times, and there are a few places where I always seem to trip myself up. Here’s what to watch out for:
- Forgetting the `needs` Keyword: If you forget to add
needs: [build-image]to the scan job, GitLab might try to run it in parallel with the build job. The scan will fail instantly because the image it’s supposed to scan doesn’t exist yet. The `needs` keyword explicitly creates that dependency. - Authentication Issues with the Registry: The Trivy job needs to pull the image from your private GitLab registry. That’s why we pass the `TRIVY_` variables. Trivy automatically picks these up to authenticate. Forgetting them is a common cause of “image not found” errors.
- Slow Scans from Uncached Databases: The first time Trivy runs, it has to download its vulnerability database. This can add a few minutes to your pipeline. You can speed this up significantly by using GitLab’s `cache` feature to store the database between runs. Just add this to your `container-scan` job:
cache: key: trivy-db paths: - /root/.cache/trivy
Conclusion
And that’s it! With a few dozen lines of YAML, you’ve moved from a manual, error-prone security check to a fully automated security gate right inside your CI/CD pipeline. This doesn’t just make your life easier; it builds a culture of security by giving developers immediate feedback on the dependencies they introduce. It shortens the feedback loop and makes security a shared responsibility, which is the core principle of DevSecOps.
Now, go get that time back in your week. Cheers,
Darian Vance
Senior DevOps Engineer, TechResolve
🤖 Frequently Asked Questions
âť“ How can I automate Docker image scanning in GitLab CI pipelines?
Automate Docker image scanning in GitLab CI by defining `build`, `scan`, and `deploy` stages. Use a `build-image` job to build and push the Docker image to the GitLab Container Registry, then a `container-scan` job with Trivy to pull and scan the image, failing the pipeline on critical/high vulnerabilities and generating a `gl-container-scanning-report.json` artifact for GitLab’s Security Dashboard.
âť“ Why choose Trivy for container scanning in GitLab CI?
Trivy is recommended for its speed, simplicity, and excellent integration with GitLab CI. It can easily scan container images for vulnerabilities, authenticate with private registries using CI/CD variables, and generate reports in a format (`gl-container-scanning-report.json`) that GitLab automatically recognizes for its Security Dashboard and Merge Request widgets.
âť“ What is a common implementation pitfall when setting up automated Docker image scanning in GitLab CI, and how is it resolved?
A common pitfall is forgetting the `needs` keyword for the scan job, which can cause it to run in parallel with the build job and fail because the image it’s supposed to scan doesn’t exist yet. This is resolved by explicitly adding `needs: [build-image]` to the `container-scan` job, ensuring the build job completes successfully before the scan job starts.
Leave a Reply