The Trivy vulnerability database is a critical component for security scanning in CI/CD pipelines, but it’s notoriously prone to falling out of date, leading to missed vulnerabilities.

Here’s a typical scenario:

# .gitlab-ci.yml example
scan-app:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy fs --security-checks vuln --exit-code 1 .

When this job runs, trivy fetches its vulnerability database. If the database hasn’t been updated recently, it won’t know about the latest CVEs. This means your scan might pass, but your application is actually vulnerable.

The core problem is that trivy fs (or trivy image, trivy rootfs, etc.) by default downloads the latest vulnerability database at runtime. This is convenient for interactive use but problematic for CI where you want deterministic, repeatable scans and often run jobs with limited network access or on ephemeral runners. The latest tag on the aquasec/trivy image doesn’t guarantee the database inside is fresh, only that the Trivy binary is the latest version.

Here’s how to ensure your Trivy scans in CI are always using the freshest vulnerability data:

1. Explicitly Update the Database at the Start of Your CI Job

The simplest and most robust method is to force Trivy to download the latest database before it performs the scan.

  • Diagnosis: Run your CI job. If it passes, manually check a known recent vulnerability (e.g., a CVE published yesterday) against your application using trivy <your-target> on your local machine. If your local scan finds it and the CI scan didn’t, the CI database was stale.

  • Fix: Add an explicit update command before your scan.

    # .gitlab-ci.yml example
    scan-app:
      stage: scan
      image: aquasec/trivy:0.49.1 # Pin to a specific version of Trivy binary
      script:
        - trivy image --download-db-only # Download the latest DB
        - trivy image --security-checks vuln --exit-code 1 your-docker-image:latest # Then scan
    
    • Why it works: The trivy image --download-db-only command ensures that the vulnerability database is downloaded and updated to the absolute latest version available from Aqua Security’s servers before the actual scanning command is executed. Pinning the Trivy image version (aquasec/trivy:0.49.1) is also good practice for CI to prevent unexpected changes from image updates.

2. Cache the Vulnerability Database

CI runners are often ephemeral. Downloading the database on every single run can be slow and consume bandwidth. Caching the database can significantly speed up subsequent scans.

  • Diagnosis: Observe the CI job logs. Notice the time spent downloading the vulnerability database, especially on subsequent pipeline runs for the same branch or tag.

  • Fix: Configure your CI/CD system to cache the Trivy database directory. The database is stored in ~/.cache/trivy by default.

    GitLab CI Example:

    # .gitlab-ci.yml
    variables:
      TRI_CACHE_DIR: "$CI_PROJECT_DIR/.cache/trivy" # Define a project-specific cache path
    
    cache:
      key: ${CI_COMMIT_REF_SLUG} # Cache per branch
      paths:
        - .cache/trivy/
    
    scan-app:
      stage: scan
      image: aquasec/trivy:0.49.1
      script:
        # Trivy will automatically use the cache if it exists and is valid
        # If the cache is missing or invalid, it will download to the cache dir
        - trivy image --security-checks vuln --exit-code 1 your-docker-image:latest
    

    GitHub Actions Example:

    # .github/workflows/ci.yml
    name: CI Scan
    
    jobs:
      scan:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Setup Trivy cache
            uses: actions/cache@v3
            id: trivy-cache
            with:
              path: |
                ~/.cache/trivy
    
              key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }} # Example key, adjust as needed
    
              restore-keys: |
    
                ${{ runner.os }}-
    
    
          - name: Run Trivy
            uses: aquasecurity/trivy-action@master # Using trivy-action simplifies this
            with:
              image-ref: 'your-docker-image:latest'
              format: 'table'
              ignore-unfixed: true
              vuln-type: 'os,library'
              # The trivy-action automatically handles DB updates and caching
              # by default, if the cache is configured correctly.
              # If you were running trivy directly:
              # - name: Install Trivy
              #   run: |
              #     apt-get update && apt-get install -y wget apt-transport-https gnupg lsb-release
              #     wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
              #     echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | tee /etc/apt/sources.list.d/trivy.list
              #     apt-get update && apt-get install -y trivy
              # - name: Trivy Scan
              #   run: trivy image --download-db-only && trivy image --security-checks vuln --exit-code 1 your-docker-image:latest
    
    • Why it works: By caching the ~/.cache/trivy directory, subsequent CI runs will first attempt to restore this directory from the cache. If a valid cache exists (based on the defined key), Trivy will find its database files already present and skip the download. If the cache is missed or invalidated, Trivy will download the database into the cache directory, which will then be uploaded for future runs. The trivy-action in GitHub Actions abstracts much of this, but understanding the underlying cache directory is key.

3. Use a Custom Trivy Docker Image with Pre-loaded Database

For maximum control and offline scenarios, you can build your own Docker image that includes a recent vulnerability database.

  • Diagnosis: You need to run scans in an environment with no or very limited internet access, or you want absolute certainty about the database version used without relying on external downloads during the pipeline.

  • Fix: Create a Dockerfile that starts from a Trivy base image and then downloads the database. Commit this image to your own registry.

    # Dockerfile
    FROM aquasec/trivy:0.49.1
    
    # Download the vulnerability database during the image build
    RUN trivy image --download-db-only
    

    Build and push this image:

    docker build -t my-registry/my-trivy-scanner:latest .
    docker push my-registry/my-trivy-scanner:latest
    

    Then, use this custom image in your CI:

    # .gitlab-ci.yml
    scan-app:
      stage: scan
      image: my-registry/my-trivy-scanner:latest # Use your custom image
      script:
        - trivy image --security-checks vuln --exit-code 1 your-docker-image:latest
    
    • Why it works: The RUN trivy image --download-db-only command executes during the docker build process. This means the vulnerability database is downloaded once when the image is built and baked into the image layers. When this custom image is used in CI, the database is already present and doesn’t need to be downloaded at runtime, ensuring a consistent and up-to-date database for every scan.

4. Scheduled Database Updates (Less Common for CI)

While not directly for CI runtime, some organizations might set up a separate CI job or a cron job to periodically update a Trivy database file and store it as an artifact or in cloud storage. The CI job would then download this pre-updated artifact instead of fetching from the live source. This is more complex and usually overkill if caching or explicit updates are sufficient.

The most common and recommended approach for CI is Option 1 (Explicit Update) combined with Option 2 (Caching). This balances freshness, speed, and simplicity. If you have strict offline requirements, Option 3 (Custom Image) is the way to go.

The next common issue you’ll encounter after ensuring your Trivy database is fresh is dealing with false positives or vulnerabilities in third-party dependencies that are outside your direct control.

Want structured learning?

Take the full Trivy course →