Skip to main content

airgap-bundle

Package a Python ML inference project (source, dependencies, container images, and model weights) into a single verifiable archive that transfers across an air gap and installs on a disconnected host with one command.

Dependency resolution assumes a network; verification assumes you can enumerate and hash every artifact before it touches the high side; and installers love to reach out for "just one thing." airgap-bundle closes those gaps: it resolves the full transitive dependency closure for the target platform, vendors images and models alongside it, produces a hashed manifest a reviewer can audit without executing anything, and fails loudly rather than silently degrading if the install path attempts the network.

Install

pip install airgap-bundle
# or, as an isolated CLI tool:
uv tool install airgap-bundle

Or from source:

git clone https://github.com/ziglicis/airgap-bundle
cd airgap-bundle
uv tool install .

Requires Python 3.11+ and uv. Building bundles also needs pip (cross-platform wheel download). Optional features shell out to standard tools, required only when that feature is used:

Feature Tool
Container images docker/podman, or skopeo (preferred)
Signing / verifying signatures cosign
SBOM generation syft
CVE gating grype

Build a bundle (low side)

On the connected build host, point it at a project with an airgap.toml:

airgap-bundle build path/to/project --out dist/app.tar.zst

This resolves a hash-locked dependency closure for the target platform, downloads the wheels, exports any declared images and models, scans the source for secrets, and writes a reproducible .tar.zst containing a manifest.json inventory and a coreutils-verifiable checksums.txt. Before it emits anything, it verifies the bundle against its own manifest; a bundle that cannot verify itself is never written.

Useful flags:

airgap-bundle build proj --platform linux/arm64        # cross-build for another arch
airgap-bundle build proj --sbom both                   # SPDX + CycloneDX SBOM (syft)
airgap-bundle build proj --sign --signing-key ck.key   # detached cosign signature

Verify and inspect (the reviewer's tools)

Read-only, never executes bundle content, never extracts to a persistent location:

airgap-bundle verify app.tar.zst --deep               # structural + integrity + deep digests
airgap-bundle verify app.tar.zst --policy policy.yaml # + license/size/sdist policy
airgap-bundle verify app.tar.zst --pubkey ck.pub      # + cosign signature (offline)
airgap-bundle inspect app.tar.zst --packages          # contents without extracting

verify emits a checklist (or --json) and exits non-zero on the first failing level: integrity (6), signature (7), policy (8).

Install a bundle (high side)

Move dist/app.tar.zst across the air gap. On the disconnected host you have two paths.

With airgap-bundle present — runs under a network guard, loads bundled images, and writes an audit receipt:

airgap-bundle install app.tar.zst --prefix /opt/app

With nothing but coreutils + Python — extract and run the bundled bootstrap. It needs no airgap-bundle and no network:

tar --zstd -xf app.tar.zst
sh app-*/install.sh /opt/app

Either path creates a virtualenv, installs the vendored wheels with --no-index, wires up the project's entrypoints, and (where a runtime is present) loads the container images (entirely offline).

What's in a bundle

app-1.0.0/
├── manifest.json          # authoritative inventory: every artifact, hash, and size
├── checksums.txt          # sha256sum -c verifiable with coreutils alone
├── install.sh             # POSIX-sh offline bootstrap
├── python/
│   ├── requirements.lock  # fully pinned, hash-locked (the reproducibility anchor)
│   └── wheels/<platform>/  # the transitive closure, target-platform wheels
├── project/               # the project's own wheel, if it is packaged
├── source/                # vendored project source
├── images/                # container images, docker/oci archives, zstd-compressed
├── models/                # fetched model weights, hash-verified
└── sbom/                  # SPDX/CycloneDX SBOM + per-image SBOMs (if --sbom)

Configuration

A project declares what to bundle in airgap.toml:

[bundle]
name = "app"
version = "1.0.0"

[platform]
targets = ["linux/amd64"]
python = "3.11"

[python]
requirements = "requirements.txt"

[source]
include = ["src/**"]

[install]
prefix = "/opt/app"
entrypoints = ["app = app.cli:main"]

[[images]]
ref = "docker.io/library/redis:7.2-alpine"

[[models]]
name = "detector"
uri = "https://models.example.com/detector.onnx"
sha256 = "3f8a…"
dest = "models/detector.onnx"

[policy]
require_cve_scan = true
cve_fail_on = "high"

Supply-chain security

  • Secret scanning - the build scans vendored source for private keys, cloud access keys, and high-entropy tokens in credential-named files, and aborts (exit 10). Overridable only by an explicit --allow-secrets, which is recorded in the manifest; never by config.
  • SBOM - --sbom spdx|cyclonedx|both runs syft over the staging tree and every bundled image, with pinned schema versions (SPDX 2.3, CycloneDX 1.5).
  • Signing - --sign produces a detached cosign signature over the archive; verify --pubkey checks it fully offline (key-based, no transparency log needed).
  • CVE gating - with policy.require_cve_scan, grype scans the SBOM and fails the build on any finding at or above policy.cve_fail_on.

Guarantees

  • Reproducible - deterministic tar (sorted, pinned mtimes, zeroed ownership) and a content-addressed bundle id; a pure-Python bundle built twice from the same inputs is byte-identical. (Image tars and SBOMs carry documented non-determinism and are excluded from the strict guarantee; the reproducibility test builds the minimal and native-deps fixtures twice and asserts byte-identical output.)
  • Verifiable - every file is hashed in the manifest and in checksums.txt; a reviewer can enumerate and check everything, and validate a signature, without executing content.
  • Isolated - a guarded install runs in a network namespace (or an audited offline fallback) and refuses to reach the network. Proven by an acceptance test that installs a bundle in a --network none container and asserts no connection was attempted.

Threat model (scope)

airgap-bundle defends the transfer and install boundary: it ensures the artifact a reviewer approved is byte-for-byte what installs on the high side, that it carries a signed, hashed inventory, and that install cannot silently reach the network. It is not a runtime sandbox, does not attest the build environment itself, and (by design for air-gap) verifies key-based signatures without a transparency log. VEX documents and in-toto build attestation are recognized next steps, not yet implemented.

Documentation

  • Architecture & threat model: the build pipeline, verify levels, install flow, reproducibility, and what the tool does and does not defend.

Status

The full command set: build, install, verify, inspect, init, diff; works today, along with cross-architecture builds, container images, model fetching, the supply-chain layer (secret scan, SBOM, signing, CVE gating), and byte-identical reproducible builds. Published on PyPI.

License

Apache-2.0

Metadata

Release files for airgap-bundle 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for airgap-bundle 0.1.1
File Size Uploaded
airgap_bundle-0.1.1.tar.gz 165.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for airgap-bundle 0.1.1
File Interpreter ABI Platform
airgap_bundle-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 247.5 kB

Release files / airgap_bundle-0.1.1.tar.gz

Download URL airgap_bundle-0.1.1.tar.gz
Size 165.3 kB
Tags Source
SHA-256 checksum
How to use checksums
071f57706f579c85d77834218000f38fd353b46b4d55c81e09ca209bc396438e
BLAKE2b-256 checksum
How to use checksums
62cc3963bdc82871483d9ed43e5055ce39d19066f4724bf40ebe2c62d548111c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / airgap_bundle-0.1.1-py3-none-any.whl

Download URL airgap_bundle-0.1.1-py3-none-any.whl
Size 82.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c3f779168ae9277032dba5ba5e4224fc135f573bfded5a7de0c21894ded80efe
BLAKE2b-256 checksum
How to use checksums
f76aab31aa6d2203ecd7450571267efe15bf27be7ee4c22fe756c5cf7fe1d57e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page