skip to content

How do you keep Trivy 0.74 vulnerability scans working and current in an air-gapped CI network that cannot reach mirror.gcr.io or ghcr.io?

level: seniorimportance: should knowfreq 17%

answer

  1. copy the artifacts, keep the tag
  2. point both DB flags inward
  3. overriding drops the defaults
  4. the first run cannot skip
  5. freshness becomes your job

basics

~20 s

Mirror trivy-db:2 and trivy-java-db:1 into an internal registry and point --db-repository and --java-db-repository at it, or copy trivy.db and metadata.json into the cache and scan with --skip-db-update. Either way, refreshing the copy is now your job.

solid answer

~40 s

There are two patterns. The preferred one: a scheduled job copies `trivy-db:2` and `trivy-java-db:1` — OCI artifacts with their own layer media types — into a registry the runners can reach, and scans set `--db-repository` and `--java-db-repository` to it. Setting a flag replaces the public defaults, so nothing reaches outward, and Trivy keeps its normal refresh logic. The fallback: on a connected host run `trivy image --download-db-only` with `--cache-dir`, carry `trivy.db` and `metadata.json` into `<cache>/db/` (and `trivy-java.db` into `java-db/`), and scan with `--skip-db-update` and `--skip-java-db-update`; a first run without those files fails. Add `--offline-scan` so JAR identification does not call Maven Central, and alert on the DB's age — a frozen database passes every gate while missing every new CVE.

code

bash · 7 lines
bash
export TRIVY_CACHE_DIR=/var/cache/trivy
trivy image \
  --db-repository registry.example.internal/security/trivy-db:2 \
  --java-db-repository registry.example.internal/security/trivy-java-db:1 \
  --offline-scan \
  --skip-version-check --disable-telemetry \
  registry.example.internal/payments/api:1.8.2

go deeper

for a junior

Recall that an offline scanner needs its databases supplied internally, and that --skip-db-update only works once a database is already in the cache.

for a middle

Explain the two patterns, registry mirror versus a hand-populated cache, and the flags each needs: the two repository flags, the two skip flags and --offline-scan.

for a senior

Show the failure modes: defaults replaced not appended, no fallback on denied, non-standard media types, a missing DownloadedAt, and a frozen DB that quietly under-reports.

for a principal

Decide who owns the mirror's refresh cadence and staleness alerting across every isolated runner, and how reports record which database build they were judged against.

## What an isolated runner is missing A Trivy scan needs more than the binary. For vulnerability scanning it needs `trivy-db` (compiled advisories) and, whenever it meets a JAR, `trivy-java-db` (an index that identifies JARs). Both are **OCI artifacts** whose 0.74 defaults are `mirror.gcr.io/aquasec/...` first and `ghcr.io/aquasecurity/...` second. Java identification may also call **Maven Central**, the optional VEX Hub repository is fetched from GitHub, and the update check contacts `check.trivy.dev`. An air-gapped runner must have every one of these answered internally or switched off. ## Pattern 1: an internal registry mirror This is the pattern to prefer, because Trivy's own refresh logic keeps working. 1. On a host with outbound access, copy `ghcr.io/aquasecurity/trivy-db:2` and `ghcr.io/aquasecurity/trivy-java-db:1` into your registry with a registry tool such as ORAS or crane, **keeping the schema tags**. 2. Run that copy on a schedule. The mirror is now the only source of freshness. 3. Point scans at it with `--db-repository` and `--java-db-repository`, or `TRIVY_DB_REPOSITORY` and `TRIVY_JAVA_DB_REPOSITORY`. 4. Authenticate the way Trivy authenticates to any private registry: `trivy registry login`, Docker credentials, or `TRIVY_USERNAME` and `TRIVY_PASSWORD`. Details that bite: - **Setting the flag replaces the defaults.** The public repositories are not appended behind yours; list them explicitly if you want them, which in an air-gapped network you do not. - **Fallback is narrow.** A second listed repository is tried only after a temporary error (429, 5xx) or `BLOB_UNKNOWN`; a denied or unauthorised response ends the attempt. Two internal mirrors give resilience; a wrong credential does not fail over. - **Media types are non-standard.** The layers are `application/vnd.aquasec.trivy.db.layer.v1.tar+gzip` and `application/vnd.aquasec.trivy.javadb.layer.v1.tar+gzip`; a proxy that accepts only container-image types will reject them. - **No tag means the schema tag.** A repository given without a tag gets the schema version appended, never `latest`. ## Pattern 2: populating the cache by hand When no registry is available, carry the files in: 1. On a connected host, `trivy image --cache-dir . --download-db-only` writes `trivy.db` and `metadata.json` into the `db/` subdirectory of that cache (and `--download-java-db-only` does the same for the Java index; the two flags cannot be combined in one run). 2. Place them in the runner's cache: `<cache>/db/trivy.db` and `<cache>/db/metadata.json`, and `<cache>/java-db/trivy-java.db` with its own `metadata.json`. 3. Scan with `--skip-db-update` and `--skip-java-db-update`. The guard rails are in the source: with no DB present, `--skip-db-update` fails on the first run; with an old schema it fails too; and a DB extracted from an `oras pull` archive carries no `DownloadedAt`, so without the skip flag Trivy warns that the DB may be corrupted and tries to re-download it. ## The other outbound calls | Call | How to handle it offline | |---|---| | Maven Central lookups for JARs | `--offline-scan`; it does not affect the databases, and some dependencies may be skipped | | VEX Hub, when using `--vex repo` | self-host a copy, or `--skip-vex-repo-update` | | version check and telemetry | `--skip-version-check` and `--disable-telemetry`, both needed | | misconfiguration checks bundle | handled by IaC scanning; the binary embeds a fallback copy | Running one Trivy server that holds the database for many clients is another topology, and keeping the cache between CI jobs is a pipeline concern; both still need a fresh database from somewhere. ## Freshness is now an operational duty An air-gapped scanner never fails because its data is old; it reports fewer findings. Make staleness visible: - Read `UpdatedAt` from `trivy version --format json` on the runner, or from `metadata.json`, and alert when it exceeds your tolerance. - Record the DB's `UpdatedAt` beside each stored report, so an auditor can tell which advisories a pass was judged against. - Re-scan released images whenever the mirror refreshes, not only on rebuild. ## Checklist for a new isolated runner 1. The internal registry serves `trivy-db:2` and `trivy-java-db:1`, refreshed on a schedule, and passes the non-standard layer media types. 2. Runners have credentials for that registry and nothing else outbound. 3. `--db-repository` and `--java-db-repository` (or their environment variables) are set in shared configuration, not per job. 4. `--offline-scan`, `--skip-version-check` and `--disable-telemetry` are set, and `--vex repo` is either self-hosted or not used. 5. A first scan on a clean cache succeeds, which proves the mirror path end to end instead of a leftover cache. 6. An alert fires when `UpdatedAt` is older than the agreed tolerance. A failure at step 5 is cheap; the same failure discovered during an incident, when nobody can tell whether the scanner saw the new advisory, is not.

  • The mirror rejects Trivy with an unauthorised error. Will Trivy try the next listed repository?
    No. Trivy moves on only after a temporary error such as 429 or a 5xx, or a `BLOB_UNKNOWN`; a denied or unauthorised response fails the download and points to the troubleshooting page. Fix the credentials with `trivy registry login` or `TRIVY_USERNAME` and `TRIVY_PASSWORD`, rather than relying on fallback.
  • Why did the pre-populated cache trigger a 'DB may be corrupted' warning?
    Files extracted from an `oras pull` archive carry no `DownloadedAt` in `metadata.json`. Without `--skip-db-update`, Trivy cannot tell a hand-copied DB from a corrupted copy, warns, and tries to re-download, which fails offline. Scanning with `--skip-db-update` tells it the copy is deliberate.
  • A caching proxy serves images fine but the DB pull fails. What do you check first?
    The layer media type. `trivy-db` uses `application/vnd.aquasec.trivy.db.layer.v1.tar+gzip`, not a container-image layer type, and Trivy's documentation warns that proxies and mirrors must pass it through. A proxy that filters to image media types breaks the pull.

saying these in an interview costs you the question

  • Set --skip-db-update on a fresh runner and Trivy scans with what it has
  • With --db-repository set, Trivy still falls back to ghcr.io if the mirror is down
  • --offline-scan stops Trivy downloading its vulnerability database
  • Once mirrored, the database stays current on its own
  • Mirror the DB under a latest tag so Trivy always gets the newest
  • Copying trivy.db alone is enough; metadata.json is optional