Implement GitLab CI/CD security scanning for Docker images

Intermediate 45 min Apr 24, 2026
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -y
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 NameValueProtectedMasked
SLACK_WEBHOOK_URLYour Slack webhook URL for notificationsYesYes
MONITORING_ENDPOINTYour monitoring system API endpointNoNo
MONITORING_TOKENAPI token for monitoring systemYesYes
CI_SECURITY_TOKENGitLab API token with registry permissionsYesYes
SECURITY_THRESHOLDHIGH,CRITICALNoNo
Note: Mark sensitive tokens as both Protected and Masked to prevent exposure in pipeline logs. The CI_JOB_TOKEN is automatically available and doesn't need manual configuration.

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
Security Gate Testing: Test your security gates with a deliberately vulnerable image to ensure the pipeline properly blocks deployment. Use images like vulnerables/web-dvwa for testing purposes.

Common issues

SymptomCauseFix
Trivy scanner fails with authentication errorRegistry credentials not properly configuredVerify CI_REGISTRY_USER and CI_REGISTRY_PASSWORD variables are set correctly
Security gate blocks all deploymentsThreshold too strict for current image stateAdjust SECURITY_THRESHOLD variable or update base images
Pipeline fails with "trivy command not found"Trivy not installed on GitLab runnerUse aquasec/trivy:latest Docker image instead of local installation
Security reports not appearing in GitLabReport format incompatible with GitLabEnsure artifacts use correct GitLab security report schema
Cache directory permissions errorTrivy cache directory ownership issuesAdd chmod 777 $TRIVY_CACHE_DIR before Trivy execution
Slack notifications not sendingWebhook URL incorrect or not accessibleTest webhook URL manually and verify SLACK_WEBHOOK_URL variable
Registry cleanup fails with 403 errorInsufficient permissions for registry managementEnsure CI_SECURITY_TOKEN has Maintainer role and registry permissions
Security dashboard shows no vulnerabilitiesReport format not matching GitLab schemaValidate gitlab-security-report.json against GitLab's schema documentation

Next steps

Running this in production?

Want this handled for you? Setting up GitLab security scanning once is straightforward. Keeping it updated with the latest vulnerability databases, managing false positives, and maintaining security policies across multiple projects is the harder part. See how we run infrastructure like this for European development teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.