Vulnerability Management

How to Run Grype in Offline/Airgap Environments

A hands-on tutorial for running Grype vulnerability scans in offline and airgapped environments, including vulnerability database hosting and CI integration.

Shadab Khan
Security Engineer
5 min read

Running Grype is straightforward on a laptop with internet access. Running Grype inside an airgap where your CI runners cannot reach toolbox-data.anchore.io is where it gets interesting. This tutorial shows how to mirror the Grype vulnerability database into an internal artifact store, point Grype at it from isolated runners, and keep the database current with a scheduled sync job running on a jump host. You will end up with Grype scans that produce identical results to the online version, a documented refresh cadence, and a fallback plan when the database is stale.

Prerequisites: grype 0.80+ on internet-connected and airgapped hosts, an internal object store or artifact registry such as MinIO, Nexus, or JFrog Artifactory, and cron or a scheduler on the jump host. Time to complete: About 60 minutes for the initial setup.

Why does Grype need a database at all?

Grype matches package CPEs and PURLs from an SBOM or image against a SQLite-backed vulnerability database compiled from NVD, GitHub Security Advisories, distro advisories, and ecosystem-specific sources like PyPA. The database is refreshed upstream every 24 hours and carries a metadata.json with a timestamp. Without a database, Grype has nothing to match against. So the whole airgap problem reduces to two questions: how do you get the database inside, and how do you keep it fresh.

Check the current database state on any connected host:

grype db status

Expected output:

Built:        2025-01-27 03:14:22 +0000 UTC
Schema:       5
Location:     /home/user/.cache/grype/db/5
Checksum:     sha256:4fd8...
Status:       valid

That Built timestamp is what you will reproduce inside the airgap.

How do I mirror the Grype database from the outside?

On a jump host with outbound internet, download the current database and upload it to your internal object store. The canonical flow uses grype db update followed by packaging the on-disk cache:

grype db update
DB_DIR=$(grype db status -o json | jq -r '.location')
TS=$(date -u +%Y%m%dT%H%M%SZ)
tar -czf /tmp/grype-db-${TS}.tar.gz -C "${DB_DIR}" .
sha256sum /tmp/grype-db-${TS}.tar.gz > /tmp/grype-db-${TS}.tar.gz.sha256

# Upload to internal MinIO
mc cp /tmp/grype-db-${TS}.tar.gz \
  internal/grype-mirror/v5/grype-db-${TS}.tar.gz
mc cp /tmp/grype-db-${TS}.tar.gz.sha256 \
  internal/grype-mirror/v5/grype-db-${TS}.tar.gz.sha256
mc cp --attr "x-amz-meta-latest=true" /tmp/grype-db-${TS}.tar.gz \
  internal/grype-mirror/v5/latest.tar.gz

The latest.tar.gz pointer makes the airgap side trivially scriptable. The timestamped files give you rollback if a bad database slips through.

How do I serve the database inside the airgap?

Point Grype at your internal mirror using the GRYPE_DB_UPDATE_URL environment variable. First, publish a listing.json file to your internal mirror that describes the available databases. Grype fetches this file to decide whether an update is available:

cat > listing.json <<'EOF'
{
  "available": {
    "5": [
      {
        "built": "2025-01-27T03:14:22Z",
        "version": 5,
        "url": "https://internal.mirror.acme/grype-mirror/v5/latest.tar.gz",
        "checksum": "sha256:4fd8..."
      }
    ]
  }
}
EOF
mc cp listing.json internal/grype-mirror/listing.json

Then on your airgapped runner:

export GRYPE_DB_UPDATE_URL="https://internal.mirror.acme/grype-mirror/listing.json"
export GRYPE_DB_CA_CERT="/etc/ssl/certs/internal-ca.pem"
grype db update
grype db status

The airgapped db status output should match the jump host's output for Built and Checksum. That byte-level match is your proof that the mirror is intact.

How do I run a scan and keep CI fast?

With the database in place, a scan looks exactly like the online version. The only airgap-specific flag is --db-auto-update=false for CI runs where you do not want a database refresh in the hot path:

grype ghcr.io/acme/api:v2.4.0 \
  --db-auto-update=false \
  --scope all-layers \
  --fail-on high \
  --output sarif=grype-results.sarif

Upload the SARIF to your internal code scanning dashboard. For pipeline speed, sync the database to each runner's local cache once per 6 hours via a systemd timer rather than on every pipeline run:

# /etc/systemd/system/grype-db-sync.timer
[Unit]
Description=Sync Grype DB from internal mirror

[Timer]
OnCalendar=*:0/360
Persistent=true

[Install]
WantedBy=timers.target

And the service:

# /etc/systemd/system/grype-db-sync.service
[Service]
Type=oneshot
Environment=GRYPE_DB_UPDATE_URL=https://internal.mirror.acme/grype-mirror/listing.json
ExecStart=/usr/local/bin/grype db update

Enable with systemctl enable --now grype-db-sync.timer. Pipeline runs now see a warm cache and skip network calls entirely.

How do I know when my database is too old?

Set an age threshold in your runner and fail pipelines if the database exceeds it. A reasonable default is 7 days, since NVD delays are rare but do happen:

BUILT=$(grype db status -o json | jq -r '.built')
BUILT_EPOCH=$(date -d "${BUILT}" +%s)
NOW_EPOCH=$(date +%s)
AGE_DAYS=$(( (NOW_EPOCH - BUILT_EPOCH) / 86400 ))
if [ "${AGE_DAYS}" -gt 7 ]; then
  echo "ERROR: Grype DB is ${AGE_DAYS} days old (>7)" >&2
  exit 1
fi

This turns a silent degradation into a loud CI failure. When you see the failure, check that the jump host's sync job is running and that the mirror upload succeeded. A stuck latest.tar.gz is the single most common incident in airgap Grype deployments.

How do I handle the schema version bump?

Grype occasionally bumps the database schema (from v4 to v5 in 2023, for example). When that happens, grype db status reports "schema mismatch" and scans refuse to run. The fix is to upgrade Grype on both the jump host and the runners in the same change, then publish a new listing.json with the new schema directory. Plan for this: tie your Grype version upgrade to the schema bump announcement on the Anchore release notes, and test in staging for a week before rolling to production runners.

How Safeguard Helps

Safeguard runs its own continuously refreshed vulnerability database and provides offline sync bundles for airgapped deployments, removing the need to maintain a Grype mirror by hand. Griffin AI correlates each scan finding with reachability data from the code, so analysts inside the airgap can prioritize the handful of truly exploitable CVEs rather than triaging hundreds of noisy matches. SBOMs and policy gates work identically online and offline, and the evidence produced inside the airgap is cryptographically signed for transfer back to the parent environment. The result is a vulnerability management workflow that is as accurate behind the wire as it is on the internet.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.