airgap-bundler collects software from the internet on a connected machine, packages it into a single archive, and gives you scripts to install that archive on the airgapped side.
If you are new to this repo, the important idea is simple:
- Build side: a machine with internet access downloads artifacts and creates a bundle
- Install side: a machine inside the restricted environment unpacks that bundle and runs the included scripts
This tool does not build your final application images. It prepares the raw materials needed on the far side of the boundary.
There are two common workflows.
Use this when you only need the standard HNCD artifacts.
python3 build.py validate profiles/hncd.toml
python3 build.py build profiles/hncd.tomlUse this when you also need to seed a Spectro Cloud VerteX airgap binary.
python3 build.py validate profiles/vertex.toml
python3 build.py build profiles/vertex.tomlprofiles/vertex.toml is a superset of profiles/hncd.toml. It includes the normal HNCD content plus the extra tool support needed for the Vertex wrapper flow.
Airgapped environments have a predictable problem: the software you need lives on the internet, but the machines that must run it do not.
This tool solves that by creating a portable .tar.gz bundle containing:
- container images
- RPM packages
- OCI artifacts such as packs or charts
- static helper tools such as
skopeo - installation scripts
- checksums and provenance files
You build the bundle once on the connected side, move it across the boundary, and install from the bundle on the restricted side.
The workflow is easier to follow if you picture two separate systems.
This machine can reach the public internet.
It runs:
python3 build.py validate ...python3 build.py build ...- optionally
python3 build.py build ... --upload ...
Its job is to download everything listed in a profile and package it.
This machine cannot reach the public internet.
It receives the finished bundle, unpacks it, and runs:
./install.shfor the standard HNCD install flow./wrapper.shfirst, if this is a Vertex bundle day
Its job is to load and push the bundled content into the destination environment.
At a high level:
- Choose a profile
- Build a bundle on the connected side
- Transfer the bundle into the restricted environment
- Unpack it
- Configure
install.env - Run the included scripts
The bundle is self-describing. If S3 upload is enabled, the destination S3 path is calculated before the build starts and written into the bundle itself.
This repo currently ships two practical profiles.
Use this for routine builds.
It contains the standard HNCD content and keeps bundle size smaller.
Use this when you also need to seed a Spectro Vertex airgap binary.
It contains:
- everything in
hncd.toml orassupport for the Vertex wrapper flow- placeholders for Vertex-specific OCI artifacts when needed
If you are unsure and do not need Spectro Vertex that day, use hncd.toml.
Four main artifact types are supported.
Examples: base images such as alpine, packer, terraform, gitlab-runner
Examples: gitlab-runner, terraform, packer
Examples: Helm charts, Spectro packs, other OCI-distributed content
Examples: skopeo, and on Vertex days, oras
Static tools matter because airgapped hosts often do not have the right package repositories available. Shipping the needed binaries with the bundle makes the install flow more reliable.
Example bundled tool:
tools/skopeo
A profile is a TOML file that declares exactly what should be collected.
If you need to add or update content, the profile is usually the first place to look.
[[images]]
image = "hashicorp/packer:1.14.2"
archive = "packer-1.14.2-base.tar"
dest_repo = "packer"[[rpms]]
url = "https://rpm.releases.hashicorp.com/.../packer-1.14.2-1.x86_64.rpm"
filename = "packer-1.14.2-1.x86_64.rpm"
sha256 = "..."[[tools]]
name = "skopeo"
url = "https://github.com/lework/skopeo-binary/releases/download/v1.17.0/skopeo-linux-amd64"
filename = "skopeo"
sha256 = "e31bc3c68909b51112fd91beec651acaddc1ad0d53806aa0e791d0a9ebc4293a"[[oci_artifacts]]
ref = "ghcr.io/spectrocloud/packs/nginx:1.25.0"
archive = "spectro-pack-nginx-1.25.0.tar"
dest_repo = "spectro/packs/nginx"
kind = "pack"These are needed on the connected machine.
| Tool | Why it is needed | Required? |
|---|---|---|
| Python 3.8+ | Runs the bundler | Yes |
skopeo |
Preferred image / OCI artifact handling | Strongly recommended |
docker |
Fallback if skopeo is absent |
Fallback only |
curl |
Downloads RPMs and other files | Yes |
tar |
Creates archives | Yes |
aws CLI |
Upload to S3 | Only if using --upload |
terraform |
Provider mirroring | Only if mirroring Terraform providers |
If you are on Python earlier than 3.11, you may also need
tomli.
These are needed on the airgapped target.
| Tool | Why it is needed |
|---|---|
bash |
Runs install.sh and wrapper.sh |
docker |
Loads images; required for the Spectro binary flow |
dnf |
Installs RPMs |
aws CLI |
Gets ECR login tokens when using ECR |
sha256sum or shasum |
Verifies checksums |
Notes:
- The bundle can ship helper tools such as
skopeoandoras. - On Vertex bundle days,
wrapper.shexpectsoras1.0.0. The Vertex profile includes the bundled tarball for that.
This checks syntax and structure without downloading artifacts.
python3 build.py validate profiles/hncd.tomlor:
python3 build.py validate profiles/vertex.tomlRoutine HNCD bundle:
python3 build.py build profiles/hncd.tomlVertex bundle:
python3 build.py build profiles/vertex.tomlpython3 build.py build profiles/hncd.toml --upload --bucket my-transfer-bucketOutput lands in ./output/.
Example:
output/hncd-20260310-144018.tar.gz
output/bundle.env
bundle.env contains the bundle name, checksum, and pre-calculated S3 URLs.
Example:
BUNDLE_NAME=hncd-20260310-144018.tar.gz
BUNDLE_URL=s3://hncd-artifacts/airgap-bundles/hncd/hncd-20260310-144018.tar.gz
BUNDLE_SIDECAR_URL=s3://hncd-artifacts/airgap-bundles/hncd/hncd-20260310-144018.tar.gz.sha256
BUNDLE_SHA256=9c645978303a60226a91ff4e96c383a121d556b8404da8d85f11c1012bbca4f5bundle.env is also written during --dry-run so you can inspect the would-be values before uploading.
tar xzf hncd-20260310-144018.tar.gz
cd hncd-20260310-144018/cp install.env.example install.env
vi install.envFor many environments, the minimum useful values are:
AWS_ACCOUNT_ID=123456789012
AWS_REGION=us-gov-west-1If you are pushing to a non-ECR registry instead, you may use:
REGISTRY=registry.internal.example.com:5000
REGISTRY_AUTH=noneinstall.env sits beside install.sh and wrapper.sh and is read automatically.
Do not commit
install.env. Useinstall.env.exampleas the template.
sudo ./install.shThe installer prints a pre-flight summary before doing any real work. If a required tool is missing, it tells you what was skipped and why.
CLI flags override install.env for that run only.
sudo ./install.sh --skip-rpms
sudo ./install.sh --skip-rpms --skip-terraform --skip-oci
sudo ./install.sh --registry registry.other.example.comUse this section only when you built with profiles/vertex.toml and need to seed a Spectro Cloud VerteX airgap binary.
wrapper.sh is included in the bundle and handles the Spectro pre-seeding flow.
In simplified terms, it:
- verifies the
.binfile exists - optionally verifies a
.sha256sidecar - logs in to private ECR using
aws,oras, anddocker - exports the
ECR_*variables the Spectro scripts expect - extracts the binary with
--noexec --target <dir> - runs legacy compatibility steps if needed
- runs any custom hook scripts bundled in
hooks/wrapper.d/ - cleans up the extracted working directory
After unpacking the bundle and filling in install.env:
sudo ./wrapper.sh airgap-fips-v4.8.39.binThen continue with the normal install flow if needed:
sudo ./install.shThe Vertex section in install.env.example controls this flow.
Typical values:
AWS_ACCOUNT_ID=102015676567
AWS_REGION=us-gov-west-1
ECR_DNS_SUFFIX=amazonaws.com
VERTEX_IMAGE_BASE=spectro-images
VERTEX_PACK_BASE=spectro-packsinstall.sh uses the same AWS_REGION + ECR_DNS_SUFFIX derivation for normal ECR pushes. REGISTRY_REGION remains available only as a legacy override for ECR auth/derivation when you need it.
In the current GovCloud flow, both the image and pack registries default to the same private ECR host derived from:
<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.<ECR_DNS_SUFFIX>
Typical values:
- AWS/GovCloud:
ECR_DNS_SUFFIX=amazonaws.com - SC2S:
ECR_DNS_SUFFIX=sc2s.sgov.gov
If you need a one-off fix for a specific Spectro release, the preferred method is:
- add an executable
.shfile under:include/hooks/wrapper.d/ - rebuild the bundle
- run
wrapper.shnormally
Hooks are copied into the final bundle under:
hooks/wrapper.d/
Each hook receives the extracted airgap directory as argument 1.
A sample no-op hook is included:
hooks/wrapper.d/00-example-hook.sh
Legacy compatibility steps such as patching bin/functions.sh or running apply_pack.sh / apply_patch.sh are still supported, but they are treated as compatibility shims rather than the main extension mechanism.
If you received a bundle from Recro and need to validate it in your environment before using it, see:
docs/bundle-install-test-handoff.md
That guide covers:
- moving the bundle into your S3 location
- triggering the
bundle-install-testCI job - reading pre-flight output
- retrieving logs from failed runs
The .gitlab-ci.yml automates the build → transfer → install sequence.
In plain language:
- the
buildjob runs on a connected runner - it uploads the bundle to S3
- it publishes
bundle.envas a dotenv artifact - later jobs read
BUNDLE_URLandBUNDLE_SHA256from that artifact
By default, install and verify are manual. Set AUTO_INSTALL=true if you want install to start automatically after build.
| Tag | Where it runs | Needs |
|---|---|---|
| (none) | Anywhere | python3 |
connected |
Internet-routable environment | python3, skopeo, curl, aws CLI |
airgapped |
Inside the restricted VPC | bash, dnf, aws CLI, passwordless sudo |
Passwordless sudo is required on the airgapped runner:
# /etc/sudoers.d/gitlab-runner
gitlab-runner ALL=(ALL) NOPASSWD: ALL
| Variable | What it is for |
|---|---|
AWS_ACCESS_KEY_ID |
Connected-runner AWS credentials |
AWS_SECRET_ACCESS_KEY |
Connected-runner AWS credentials |
AWS_ACCOUNT_ID |
12-digit AWS account ID |
AWS_REGION |
Example: us-gov-west-1 |
TRANSFER_BUCKET |
S3 bucket used to move bundles |
TRANSFER_PREFIX |
S3 key prefix, such as airgap-bundles/hncd |
| Variable | Effect |
|---|---|
SKIP_IMAGES |
true to skip image pull |
SKIP_RPMS |
true to skip RPM download |
SKIP_OCI |
true to skip OCI artifact pull |
AUTO_INSTALL |
true to auto-trigger install |
ECR_DNS_SUFFIX |
Override derived ECR hostname suffix, for example sc2s.sgov.gov |
This tool gets raw materials across the boundary. It does not produce the final hardened workload images.
For example, the bundler delivers base content such as:
alpinepackerterraformgitlab-runner
After those are available in the restricted environment, another tool such as oci-image-builder can build final STIG-hardened images on top of them.
A useful mental model is:
airgap-bundlermoves ingredients- downstream image-building tools make the finished product
Bundles land at the configured prefix with a checksum sidecar.
Example:
airgap-bundles/hncd/
hncd-20260310-144018.tar.gz
hncd-20260310-144018.tar.gz.sha256
The full S3 URL is calculated at build start and written into meta/bundle-source.txt inside the archive.
You can set the destination in the profile:
[s3]
bucket = "my-transfer-bucket"
prefix = "airgap-bundles/hncd"
region = "us-gov-west-1"You can also override bucket or prefix at build time with CLI flags.
Upload an already-built bundle to S3:
python3 build.py upload output/hncd-20260306-120000.tar.gz --bucket my-bucketList bundles in S3:
python3 build.py list --bucket my-bucket- Use
profiles/hncd.tomlfor normal days andprofiles/vertex.tomlfor Vertex days. - Build on the connected side, install on the airgapped side.
- On Vertex days, run
./wrapper.shbefore./install.sh.



