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|bothrunssyftover the staging tree and every bundled image, with pinned schema versions (SPDX 2.3, CycloneDX 1.5). - Signing -
--signproduces a detachedcosignsignature over the archive;verify --pubkeychecks it fully offline (key-based, no transparency log needed). - CVE gating - with
policy.require_cve_scan,grypescans the SBOM and fails the build on any finding at or abovepolicy.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
minimalandnative-depsfixtures 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 nonecontainer 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)
| File | Size | Uploaded | |
|---|---|---|---|
| airgap_bundle-0.1.0.tar.gz | 165.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|