Security testing shouldn't start after an application reaches production.
If we are already using CI/CD to build, test and deploy our applications, it makes sense to introduce security checks into the same process. This allows us to identify vulnerabilities before they become production problems.
One of the open-source tools we can use for this is Trivy.
Trivy can scan container images, source code repositories and filesystems for vulnerabilities. It can also identify configuration problems, secrets and other security-related issues.
In this article, we will see how we can introduce Trivy into a CI/CD pipeline, starting with a simple local scan and then moving towards using it as a security gate in our pipeline.
What Is Trivy?
Trivy is an open-source security scanner maintained by Aqua Security.
It is commonly used for scanning container images, but it can do much more than that. Depending on how we use it, Trivy can scan:
- Container images
- Filesystems
- Git repositories
- Infrastructure as Code
- Kubernetes configurations
- Dependencies
- Secrets
- Licenses
For this article, we will focus mainly on container image vulnerability scanning because it is one of the easiest ways to introduce security scanning into an existing CI/CD process.
Why Add Vulnerability Scanning to CI/CD?
Let's consider a typical application pipeline.
The application is compiled, tests are executed, a Docker image is created and the image is pushed to a container registry. Eventually, that image is deployed to Kubernetes or another production environment.
The problem is that the container image may contain vulnerable operating system packages or application dependencies.
If we only discover those vulnerabilities after deployment, fixing them becomes more complicated.
Instead, we can scan the image before it is pushed or deployed.
A simple pipeline can therefore look like this:
Code
↓
Build
↓
Unit Tests
↓
Build Docker Image
↓
Trivy Scan
↓
Push Image
↓
Deploy
If the vulnerability scan fails, the pipeline stops and the vulnerable image doesn't move further through the deployment process.
This is the basic idea behind adding security into CI/CD.
Step 1: Install Trivy
There are several ways to install Trivy depending on the operating system and environment.
For Ubuntu, we can install it using the official repository.
sudo apt-get install wget gnupg
Add the repository signing key:
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
gpg --dearmor | \
sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
Add the Trivy repository:
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \
https://aquasecurity.github.io/trivy-repo/deb \
generic main" | \
sudo tee /etc/apt/sources.list.d/trivy.list
Update the package list:
sudo apt-get update
Install Trivy:
sudo apt-get install trivy
Verify the installation:
trivy --version
We should see the installed Trivy version in the output.
The installation method may change over time, so it is worth checking the current Trivy documentation if we are setting this up on a new machine.
Step 2: Scan a Docker Image
Once Trivy is installed, we can immediately start scanning container images.
For example:
trivy image nginx:latest
Trivy will download the image if it isn't already available locally and scan its packages for known vulnerabilities.
The output will contain information such as:
Library Vulnerability Severity
openssl CVE-XXXX-XXXXX HIGH
curl CVE-XXXX-XXXXX MEDIUM
libxyz CVE-XXXX-XXXXX CRITICAL
The exact results will depend on the image version and the vulnerabilities known at the time of the scan.
This is already useful, but we probably don't want every vulnerability to stop our pipeline.
Step 3: Scan Only High and Critical Vulnerabilities
In a CI/CD environment, we usually need to decide which vulnerabilities should block a deployment.
We can filter the results by severity:
trivy image --severity HIGH,CRITICAL nginx:latest
This allows us to focus on vulnerabilities that require immediate attention.
However, filtering the displayed results alone doesn't necessarily make the pipeline fail. We need to explicitly configure the exit code.
Step 4: Make the Pipeline Fail
This is where Trivy becomes useful as a CI/CD security gate.
Consider this command:
trivy image \
--severity HIGH,CRITICAL \
--exit-code 1 \
nginx:latest
The --exit-code 1 option tells Trivy to return a non-zero exit code when vulnerabilities matching the selected criteria are found.
CI/CD systems generally treat a non-zero exit code as a failed step.
So the behaviour becomes:
No HIGH/CRITICAL vulnerabilities
↓
Pipeline continues
HIGH/CRITICAL vulnerability found
↓
Trivy returns exit code 1
↓
Pipeline fails
This is the important difference between simply running a security scan and actually making security part of our deployment process.
Step 5: Scan Our Own Docker Image
Instead of scanning a public image, let's assume our pipeline builds an image called:
myapp:1.0.0
We can scan it using:
trivy image --severity HIGH,CRITICAL myapp:1.0.0
For CI/CD, we can use:
trivy image \
--severity HIGH,CRITICAL \
--exit-code 1 \
myapp:1.0.0
If the scan passes, the pipeline can continue with the next stage.
If the scan finds a HIGH or CRITICAL vulnerability, the pipeline stops.
Step 6: Ignore Vulnerabilities That Don't Have a Fix
This is one area where we need to be careful.
A vulnerability may be known, but there may not yet be a fixed package available.
If we fail the pipeline for every vulnerability regardless of whether a fix exists, developers may quickly start treating the security pipeline as an obstacle rather than a useful control.
Trivy allows us to ignore vulnerabilities for which no fix is currently available.
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 1 \
myapp:1.0.0
This means we focus the pipeline gate on vulnerabilities where a fix is available.
That doesn't mean unfixed vulnerabilities should be ignored forever. They should still be tracked and reviewed.
Step 7: Scanning the Source Code
Trivy isn't limited to container images.
We can also scan a project directory:
trivy fs .
We can focus on vulnerabilities:
trivy fs \
--scanners vuln \
.
We can also scan for secrets:
trivy fs \
--scanners secret \
.
This can help detect accidentally committed credentials, tokens and other sensitive information.
For example, a developer might accidentally commit a configuration file containing an API key.
A source scan gives us another opportunity to detect the problem before the code reaches production.
Step 8: Adding Trivy to a CI/CD Pipeline
Now we can bring everything together.
A simplified pipeline looks like this:
Checkout Code
↓
Build Application
↓
Run Tests
↓
Build Docker Image
↓
Trivy Vulnerability Scan
↓
Push Image
↓
Deploy
The important part is where we place the security scan.
We should scan the exact image that we are planning to deploy.
For example:
docker build -t myapp:$BUILD_ID .
Then:
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 1 \
myapp:$BUILD_ID
If the scan passes:
docker push myapp:$BUILD_ID
The deployment can then use that exact image.
This gives us a simple security gate before the artifact moves to the next environment.
Step 9: Generate a Report
Sometimes we don't just want the pipeline to pass or fail. We also want a report that developers and security teams can review.
Trivy supports different output formats.
For example:
trivy image \
--format json \
--output trivy-report.json \
myapp:$BUILD_ID
We can then publish the generated report as a CI/CD pipeline artifact.
This is useful because the pipeline result tells us that something failed, while the report tells us what actually needs to be fixed.
Step 10: Don't Make Every Vulnerability a Pipeline Failure
This is where security implementation requires some judgement.
If we configure the pipeline to fail for every LOW, MEDIUM, HIGH and CRITICAL vulnerability from day one, there is a good chance the pipeline will become difficult to use.
A better approach is to establish a security policy.
For example:
LOW → Report
MEDIUM → Report and Track
HIGH → Review / Block
CRITICAL → Block
The exact policy will depend on the application and organisation.
For internet-facing applications, we may choose a stricter policy. For internal applications, we may initially use a more gradual approach.
The important thing is to define the policy rather than letting every security finding become an emergency.
Managing Exceptions
There will be situations where a vulnerability needs to be accepted temporarily.
Trivy supports ignore files for this purpose.
For example:
.trivyignore
We can place vulnerability IDs in the file that we have deliberately reviewed and accepted.
However, this should be used carefully.
A common mistake is to keep adding vulnerabilities to .trivyignore simply because they are causing pipeline failures.
That defeats the purpose of having the security scan in the first place.
Every exception should have a reason, an owner and, ideally, an expiry or review date.
Common Mistakes
There are a few mistakes we should avoid when introducing Trivy into CI/CD.
Scanning Only in Production
If we scan only after deployment, we have already allowed the vulnerable artifact into our environment.
Security scanning is more useful when it happens before deployment.
Blocking Everything Immediately
Introducing a security gate without understanding the current vulnerability baseline can cause hundreds of existing issues to break the pipeline.
It is often better to establish a baseline first and then gradually increase the enforcement level.
Ignoring Unfixed Vulnerabilities Forever
Using --ignore-unfixed can make the pipeline more practical, but it shouldn't become an excuse to forget about those vulnerabilities.
The vulnerability may receive a fix later.
Ignoring the Docker Base Image
Many vulnerabilities come from the base image itself.
For example:
FROM ubuntu:latest
The application code may be perfectly secure while the underlying image contains vulnerable packages.
Keeping the base image updated is therefore an important part of container security.
Treating the Scan as the Final Security Check
Trivy is a valuable security tool, but it doesn't replace a complete security program.
Application security also includes:
- Secure coding
- Dependency management
- Secrets management
- Access control
- Network security
- Authentication
- Authorization
- Infrastructure security
- Runtime monitoring
Trivy should be one layer in the overall security process.
Where Should We Run the Scan?
There isn't one universal answer.
A practical approach is to scan at multiple stages.
For example:
Developer Machine
↓
Source / Dependency Scan
↓
CI Build
↓
Container Image Scan
↓
Container Registry
↓
Deployment
↓
Runtime Monitoring
The earlier we identify a problem, the cheaper it usually is to fix.
A vulnerability found during development is much easier to address than one discovered after a production deployment.
No comments:
Post a Comment