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

The tool is not published yet; install it 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. Not yet published to PyPI.

License

Apache-2.0

Metadata

Release files for airgap-bundle 0.1.0

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.0
File Size Uploaded
airgap_bundle-0.1.0.tar.gz 165.3 kB Details

Built distribution (wheel)

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

Total release size: 247.4 kB

Release files / airgap_bundle-0.1.0.tar.gz

Download URL airgap_bundle-0.1.0.tar.gz
Size 165.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d3bd1257ae93b11b5ae159d7d3d48d65e2803343dc69fc924236548cf7039eeb
BLAKE2b-256 checksum
How to use checksums
38d2ffe9dbe1501cd1b18432c865bc669ca1499470d733f5758184c6e23dbfdf
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.0-py3-none-any.whl

Download URL airgap_bundle-0.1.0-py3-none-any.whl
Size 82.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa66bcdaeeef203ed6e1df3abf4dccb94c9cf5e0778f0f48e1253bd4b4945d75
BLAKE2b-256 checksum
How to use checksums
8bb46eb7e5d58e20636636e1db7196f9b85da3cc574ec2af4e9197df2814855e
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

0.1.1

2 release files

This release

0.1.0 This release

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