Set up automated container vulnerability scanning in GitLab CI/CD pipelines with Trivy and registry integration. Implement security gates, quality controls, and automated reporting for production-ready DevSecOps workflows.
Prerequisites
- GitLab project with CI/CD enabled
- GitLab Runner with Docker executor
- Container registry access
- Basic Docker knowledge
What this solves
GitLab CI/CD security scanning automates vulnerability detection for Docker images before they reach production. This tutorial implements Trivy container scanning, security gates that block vulnerable deployments, and automated reporting integrated with GitLab's container registry. You'll configure pipeline stages that scan images for CVEs, enforce security policies, and generate actionable security reports for your development teams.
Step-by-step configuration
Install Trivy scanner
Install Trivy container vulnerability scanner on your GitLab runner system for image scanning capabilities.
sudo apt update
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
trivy --version
Configure GitLab container registry authentication
Set up authentication variables for GitLab container registry access during CI/CD pipeline execution.
# Add these variables in GitLab UI:
CI_REGISTRY_USER: gitlab-ci-token
CI_REGISTRY_PASSWORD: $CI_JOB_TOKEN
CI_REGISTRY: registry.gitlab.com
CI_REGISTRY_IMAGE: registry.gitlab.com/your-group/your-project
Create base GitLab CI security scanning pipeline
Configure the main GitLab CI pipeline with Docker build, vulnerability scanning, and security gate stages.
stages:
- build
- test
- security-scan
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
TRIVY_CACHE_DIR: ".trivycache/"
SECURITY_THRESHOLD: "HIGH,CRITICAL"
build-image:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- branches
trivy-vulnerability-scan:
stage: security-scan
image: aquasec/trivy:latest
cache:
paths:
- .trivycache/
before_script:
- export TRIVY_USERNAME=$CI_REGISTRY_USER
- export TRIVY_PASSWORD=$CI_REGISTRY_PASSWORD
script:
- trivy image --cache-dir $TRIVY_CACHE_DIR --format json --output trivy-report.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy image --cache-dir $TRIVY_CACHE_DIR --severity $SECURITY_THRESHOLD --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
reports:
container_scanning: trivy-report.json
paths:
- trivy-report.json
expire_in: 1 week
dependencies:
- build-image
only:
- branches
Configure advanced Trivy scanning with custom policies
Create detailed Trivy configuration file for comprehensive vulnerability scanning with custom severity thresholds and ignore rules.
cache:
dir: .trivycache/
db:
skip-update: false
light: false
vulnerability:
type:
- os
- library
scanners:
- vuln
- secret
- config
severity:
- UNKNOWN
- LOW
- MEDIUM
- HIGH
- CRITICAL
format: json
timeout: 10m
ignore-unfixed: false
ignore-policy: .trivyignore
output: trivy-detailed-report.json
Create vulnerability ignore policy
Define ignored vulnerabilities with justification comments for security compliance tracking.
# Ignore specific CVE with business justification
CVE-2023-12345: Not applicable to our use case - library not used in production code
CVE-2023-12345
Temporary ignore for development dependencies
Will be addressed in next security sprint
CVE-2023-67890
Base image vulnerabilities - waiting for upstream fix
Tracking in security backlog item SEC-123
CVE-2023-11111
Implement security quality gates
Configure pipeline stages with security thresholds that prevent deployment of vulnerable images to production environments.
security-gate-staging:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --config trivy.yaml --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- echo "Security gate passed for staging deployment"
allow_failure: false
dependencies:
- build-image
only:
- develop
- staging
security-gate-production:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --config trivy.yaml --severity MEDIUM,HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy image --config trivy.yaml --scanners secret --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- echo "Security gate passed for production deployment"
allow_failure: false
dependencies:
- build-image
only:
- main
- production
generate-security-report:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --config trivy.yaml --format table --output security-summary.txt $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy image --config trivy.yaml --format sarif --output security-sarif.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy image --config trivy.yaml --format cyclonedx --output security-sbom.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
reports:
sast: security-sarif.json
cyclonedx: security-sbom.json
paths:
- security-summary.txt
- security-sarif.json
- security-sbom.json
expire_in: 30 days
dependencies:
- build-image
only:
- branches
Configure GitLab container registry scanning integration
Enable GitLab's native container registry scanning features with custom scanner configuration for comprehensive vulnerability tracking.
include:
- template: Security/Container-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
container_scanning:
stage: security-scan
variables:
CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
CS_REGISTRY_USER: $CI_REGISTRY_USER
CS_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
CS_ANALYZER_IMAGE: registry.gitlab.com/security-products/container-scanning:5
CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE:latest
CS_DISABLE_DEPENDENCY_LIST: "false"
CS_DOCKER_INSECURE: "false"
CS_SEVERITY_THRESHOLD: "MEDIUM"
dependencies:
- build-image
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
paths:
- gl-container-scanning-report.json
only:
- branches
Set up automated security reporting and notifications
Configure automated security report generation and team notifications for vulnerability findings and security gate failures.
security-report-slack:
stage: security-scan
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- |
if [ -f trivy-report.json ]; then
CRITICAL_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' trivy-report.json)
HIGH_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' trivy-report.json)
if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 0 ]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"🚨 Security Alert: $CI_PROJECT_NAME pipeline found $CRITICAL_COUNT critical and $HIGH_COUNT high severity vulnerabilities in commit $CI_COMMIT_SHORT_SHA. Pipeline: $CI_PIPELINE_URL\"}" \
$SLACK_WEBHOOK_URL
fi
fi
dependencies:
- trivy-vulnerability-scan
when: always
only:
- main
- develop
security-metrics-dashboard:
stage: security-scan
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- |
# Generate security metrics for dashboard
if [ -f trivy-report.json ]; then
echo "Generating security metrics..."
jq -r '.Results[]?.Vulnerabilities[]? | [.VulnerabilityID, .Severity, .PkgName, .InstalledVersion] | @csv' trivy-report.json > security-metrics.csv
# Send to monitoring system (example with curl)
curl -X POST "$MONITORING_ENDPOINT/security-metrics" \
-H "Authorization: Bearer $MONITORING_TOKEN" \
-F "file=@security-metrics.csv" \
-F "project=$CI_PROJECT_NAME" \
-F "branch=$CI_COMMIT_REF_NAME" \
-F "commit=$CI_COMMIT_SHA"
fi
artifacts:
paths:
- security-metrics.csv
expire_in: 7 days
dependencies:
- trivy-vulnerability-scan
when: always
only:
- main
Configure container registry cleanup policies
Set up automated cleanup for scanned container images to manage registry storage efficiently while maintaining security audit trails. This complements GitLab container registry cleanup policies with security-specific retention rules.
registry-cleanup:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- |
# Keep images with passing security scans longer
# Clean up failed security scan images after 7 days
echo "Implementing security-aware cleanup policy..."
# This would integrate with GitLab API to manage registry
curl --header "PRIVATE-TOKEN: $CI_SECURITY_TOKEN" \
--request PUT "$CI_API_V4_URL/projects/$CI_PROJECT_ID/registry/repositories/$REGISTRY_ID" \
--data 'expiration_policy_started=true' \
--data 'keep_n=10' \
--data 'older_than=7d' \
--data 'name_regex_delete=.failed-security.'
when: manual
only:
- main
Enable GitLab Security Dashboard integration
Configure pipeline to populate GitLab's Security Dashboard with vulnerability findings for centralized security monitoring across projects.
security-dashboard:
stage: security-scan
image: alpine:latest
before_script:
- apk add --no-cache curl jq
script:
- |
# Convert Trivy report to GitLab Security format
if [ -f trivy-report.json ]; then
echo "Converting security report for GitLab Dashboard..."
# Create GitLab-compatible vulnerability report
jq '{
"version": "15.0.4",
"vulnerabilities": [
.Results[]?.Vulnerabilities[]? | {
"id": .VulnerabilityID,
"category": "container_scanning",
"name": .Title,
"message": .Description,
"severity": (.Severity | ascii_downcase),
"solution": .FixedVersion,
"scanner": {
"id": "trivy",
"name": "Trivy"
},
"location": {
"dependency": {
"package": {
"name": .PkgName
},
"version": .InstalledVersion
},
"operating_system": "linux",
"image": "'$CI_REGISTRY_IMAGE':'$CI_COMMIT_SHA'"
},
"identifiers": [
{
"type": "cve",
"name": .VulnerabilityID,
"value": .VulnerabilityID,
"url": .PrimaryURL
}
]
}
]
}' trivy-report.json > gitlab-security-report.json
fi
artifacts:
reports:
container_scanning: gitlab-security-report.json
paths:
- gitlab-security-report.json
dependencies:
- trivy-vulnerability-scan
only:
- branches
Configure security scanning variables
Set up GitLab CI/CD variables for security scanning configuration and authentication. Navigate to your GitLab project's Settings > CI/CD > Variables section.
| Variable Name | Value | Protected | Masked |
|---|---|---|---|
| SLACK_WEBHOOK_URL | Your Slack webhook URL for notifications | Yes | Yes |
| MONITORING_ENDPOINT | Your monitoring system API endpoint | No | No |
| MONITORING_TOKEN | API token for monitoring system | Yes | Yes |
| CI_SECURITY_TOKEN | GitLab API token with registry permissions | Yes | Yes |
| SECURITY_THRESHOLD | HIGH,CRITICAL | No | No |
Verify your setup
Test the security scanning pipeline with a sample Docker image build and verify all components function correctly.
# Check Trivy installation
trivy --version
Test local image scanning
docker build -t test-image .
trivy image test-image
Verify GitLab pipeline execution
git add .
git commit -m "Add security scanning pipeline"
git push origin main
Check pipeline status in GitLab UI
echo "Visit: https://gitlab.com/your-group/your-project/-/pipelines"
Verify security reports generation
ls -la trivy-report.json security-*.json gitlab-security-report.json
vulnerables/web-dvwa for testing purposes.Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Trivy scanner fails with authentication error | Registry credentials not properly configured | Verify CI_REGISTRY_USER and CI_REGISTRY_PASSWORD variables are set correctly |
| Security gate blocks all deployments | Threshold too strict for current image state | Adjust SECURITY_THRESHOLD variable or update base images |
| Pipeline fails with "trivy command not found" | Trivy not installed on GitLab runner | Use aquasec/trivy:latest Docker image instead of local installation |
| Security reports not appearing in GitLab | Report format incompatible with GitLab | Ensure artifacts use correct GitLab security report schema |
| Cache directory permissions error | Trivy cache directory ownership issues | Add chmod 777 $TRIVY_CACHE_DIR before Trivy execution |
| Slack notifications not sending | Webhook URL incorrect or not accessible | Test webhook URL manually and verify SLACK_WEBHOOK_URL variable |
| Registry cleanup fails with 403 error | Insufficient permissions for registry management | Ensure CI_SECURITY_TOKEN has Maintainer role and registry permissions |
| Security dashboard shows no vulnerabilities | Report format not matching GitLab schema | Validate gitlab-security-report.json against GitLab's schema documentation |
Next steps
- Configure Falco runtime security for Kubernetes threat detection
- Implement Airflow DAG security scanning with Bandit and safety checks
- Configure Podman image scanning with Trivy security vulnerability detection
- Set up GitLab backup and disaster recovery with automated restoration
- Implement GitLab CI/CD security policy management with OPA and compliance scanning
- Configure GitLab CI/CD container registry mirroring for security and performance
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# GitLab CI/CD Security Scanning Setup Script
# Installs Trivy scanner and configures GitLab CI security scanning
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
TOTAL_STEPS=7
TRIVY_VERSION="latest"
GITLAB_PROJECT_PATH=""
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
rm -f /tmp/trivy-install.sh
exit 1
}
trap cleanup ERR
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Install and configure GitLab CI/CD security scanning with Trivy
OPTIONS:
-p, --project PATH GitLab project path (group/project-name)
-h, --help Show this help message
EXAMPLES:
$0 -p mygroup/myproject
$0 --project company/webapp
EOF
}
log_step() {
echo -e "${BLUE}[$1/$TOTAL_STEPS] $2${NC}"
}
log_success() {
echo -e "${GREEN}[SUCCESS] $1${NC}"
}
log_warning() {
echo -e "${YELLOW}[WARNING] $1${NC}"
}
log_error() {
echo -e "${RED}[ERROR] $1${NC}"
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--project)
GITLAB_PROJECT_PATH="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ -z "$GITLAB_PROJECT_PATH" ]]; then
log_error "GitLab project path is required"
usage
exit 1
fi
# Check prerequisites
log_step 1 "Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
SUDO_CMD=""
else
if ! command -v sudo &> /dev/null; then
log_error "This script requires sudo privileges"
exit 1
fi
SUDO_CMD="sudo"
fi
# Detect distribution
if [[ ! -f /etc/os-release ]]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_success "Detected $PRETTY_NAME with $PKG_MGR package manager"
# Update package manager
log_step 2 "Updating package manager..."
$SUDO_CMD $PKG_UPDATE
# Install dependencies
log_step 3 "Installing dependencies..."
$SUDO_CMD $PKG_INSTALL curl wget git
# Install Trivy
log_step 4 "Installing Trivy scanner..."
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh -o /tmp/trivy-install.sh
$SUDO_CMD sh /tmp/trivy-install.sh -b /usr/local/bin
rm -f /tmp/trivy-install.sh
# Verify Trivy installation
if ! command -v trivy &> /dev/null; then
log_error "Trivy installation failed"
exit 1
fi
TRIVY_INSTALLED_VERSION=$(trivy --version | head -n1 | awk '{print $2}')
log_success "Trivy $TRIVY_INSTALLED_VERSION installed successfully"
# Create GitLab CI configuration
log_step 5 "Creating GitLab CI configuration..."
cat > .gitlab-ci.yml << EOF
stages:
- build
- test
- security-scan
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
TRIVY_CACHE_DIR: ".trivycache/"
SECURITY_THRESHOLD: "HIGH,CRITICAL"
build-image:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- echo \$CI_REGISTRY_PASSWORD | docker login -u \$CI_REGISTRY_USER --password-stdin \$CI_REGISTRY
script:
- docker build -t \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA .
- docker push \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA
only:
- branches
trivy-vulnerability-scan:
stage: security-scan
image: aquasec/trivy:latest
cache:
paths:
- .trivycache/
before_script:
- export TRIVY_USERNAME=\$CI_REGISTRY_USER
- export TRIVY_PASSWORD=\$CI_REGISTRY_PASSWORD
script:
- trivy image --cache-dir \$TRIVY_CACHE_DIR --format json --output trivy-report.json \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA
- trivy image --cache-dir \$TRIVY_CACHE_DIR --severity \$SECURITY_THRESHOLD --exit-code 1 \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA
artifacts:
reports:
container_scanning: trivy-report.json
paths:
- trivy-report.json
expire_in: 1 week
dependencies:
- build-image
only:
- branches
security-gate-staging:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --severity CRITICAL --exit-code 1 \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA
only:
- develop
- staging
security-gate-production:
stage: security-scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 \$CI_REGISTRY_IMAGE:\$CI_COMMIT_SHA
only:
- main
- master
EOF
log_success "Created .gitlab-ci.yml configuration"
# Create Trivy configuration
log_step 6 "Creating Trivy configuration files..."
cat > trivy.yaml << EOF
cache:
dir: .trivycache/
db:
skip-update: false
light: false
vulnerability:
type:
- os
- library
scanners:
- vuln
- secret
- config
severity:
- UNKNOWN
- LOW
- MEDIUM
- HIGH
- CRITICAL
format: json
timeout: 10m
ignore-unfixed: false
ignore-policy: .trivyignore
output: trivy-detailed-report.json
EOF
cat > .trivyignore << EOF
# Security ignore policy for $GITLAB_PROJECT_PATH
# Add CVEs to ignore with proper justification
# Example entries (remove these and add your own):
# CVE-2023-12345 # Not applicable - library not used in production
# CVE-2023-67890 # Development dependency only - will fix in next sprint
EOF
chmod 644 .gitlab-ci.yml trivy.yaml .trivyignore
log_success "Created Trivy configuration files"
# Create documentation
log_step 7 "Creating documentation..."
cat > SECURITY_SCANNING.md << EOF
# GitLab CI/CD Security Scanning
This project is configured with automated security scanning using Trivy.
## Configuration
- **Pipeline file**: \`.gitlab-ci.yml\`
- **Trivy config**: \`trivy.yaml\`
- **Ignore policy**: \`.trivyignore\`
## GitLab Variables Required
Set these variables in GitLab UI (Settings > CI/CD > Variables):
| Variable | Value | Protected | Masked |
|----------|-------|-----------|---------|
| CI_REGISTRY_USER | gitlab-ci-token | No | No |
| CI_REGISTRY_PASSWORD | \$CI_JOB_TOKEN | No | Yes |
| CI_REGISTRY | registry.gitlab.com | No | No |
| CI_REGISTRY_IMAGE | registry.gitlab.com/$GITLAB_PROJECT_PATH | No | No |
## Security Gates
- **Staging**: Blocks CRITICAL vulnerabilities
- **Production**: Blocks HIGH and CRITICAL vulnerabilities
## Managing Vulnerabilities
1. Review scan results in GitLab Security Dashboard
2. Add justified ignores to \`.trivyignore\` file
3. Update base images and dependencies regularly
4. Monitor security reports for new vulnerabilities
## Local Scanning
Run local scans before pushing:
\`\`\`bash
# Scan local image
trivy image your-image:tag
# Scan with config file
trivy image --config trivy.yaml your-image:tag
\`\`\`
EOF
chmod 644 SECURITY_SCANNING.md
log_success "Created documentation"
# Final verification
echo ""
echo "=== Installation Complete ==="
echo ""
log_success "Trivy scanner installed: $(trivy --version | head -n1)"
log_success "GitLab CI configuration created: .gitlab-ci.yml"
log_success "Trivy configuration created: trivy.yaml"
log_success "Security ignore policy created: .trivyignore"
log_success "Documentation created: SECURITY_SCANNING.md"
echo ""
echo -e "${YELLOW}Next Steps:${NC}"
echo "1. Commit the generated files to your GitLab repository"
echo "2. Configure the required GitLab CI/CD variables in your project settings"
echo "3. Create a Dockerfile if you don't have one"
echo "4. Push changes to trigger the security scanning pipeline"
echo ""
echo -e "${BLUE}GitLab Variables to Set:${NC}"
echo "- CI_REGISTRY_USER: gitlab-ci-token"
echo "- CI_REGISTRY_PASSWORD: \$CI_JOB_TOKEN"
echo "- CI_REGISTRY: registry.gitlab.com"
echo "- CI_REGISTRY_IMAGE: registry.gitlab.com/$GITLAB_PROJECT_PATH"
Review the script before running. Execute with: bash install.sh