Yabanin offline (air-gap) bundle — build & verify
How to produce a self-contained Yabanin bundle on a connected machine and
verify/install it on a machine without any network. The bundle uses only
artifacts that make release-artifacts and docker build already produce:
binaries + SHA256SUMS, SBOM, optional cosign signature, and saved container
images. Kubernetes-based air-gap installs are covered separately in
install-guide.md and airgap-checklist.md;
this document is the minimal Docker/compose path.
Bundle contents
| File | Produced by | Purpose |
|---|---|---|
dist/release/yabanin-* | make release-artifacts | static service binaries (-trimpath, stamped version/commit) |
dist/release/SHA256SUMS | make release-artifacts | sha256 over all binaries |
dist/release/SHA256SUMS.sig | make release-artifacts (only when COSIGN_KEY is set) | cosign signature over SHA256SUMS |
dist/release/sbom.spdx.json or dist/release/*.buildinfo | make release-artifacts | SBOM (syft SPDX), or go version -m build manifests as fallback when syft is not installed |
images.tar.gz | docker save | hardened Yabanin image + pgvector/pgvector:pg16 + redis:7-alpine |
manifest.sha256 | sha256sum | checksums over every file in the bundle (tamper check before install) |
yabanin-release.pub | cosign public-key | public key matching COSIGN_KEY (public material only) |
compose/airgap-compose.yml | deploy/onprem/airgap-compose.yml | minimal compose with no build: and no registry references |
No secrets are ever placed in the bundle: no COSIGN_KEY, no .env, no
passwords. Only the public key is shipped.
Prerequisites
On the connected build machine:
goandmake(formake release-artifacts);docker(image build +docker save);cosign— optional but required for a signed bundle; if not installed,make release-artifactsskips signing and the release must be marked unsigned in the release notes;syft— optional; if not installed, the SBOM falls back togo version -mbuild manifests (.buildinfoper binary), which is honest build metadata but not a full SPDX SBOM;COSIGN_KEY— your private signing key path/refs when signing.
In the repository authoring environment at the time of writing, cosign and
syft are not installed; treat them as prerequisites to install before
producing a signed release with a full SPDX SBOM. Without them the bundle is
still verifiable by hashes, and airgap-verify.sh reports the signature stage
as SKIP (unsigned) instead of hiding it.
Step 1 — Build release artifacts (connected machine)
# Without signing:
make release-artifacts
# With signing (preferred for air-gap delivery):
COSIGN_KEY=/secure/path/cosign.key make release-artifacts
cosign public-key --key /secure/path/cosign.key > dist/release/yabanin-release.pub
The target also smoke-tests every binary (checksum re-verified in a clean dir, build info read, binary executed against an invalid config — fast local failure, no network).
Note: the service binaries do not support --help; the fast "does it run"
check for them is a deliberately invalid config, which must exit non-zero
(this is exactly what make release-artifacts does internally).
Step 2 — Build and save images (connected machine)
VERSION=$(git describe --tags --always)
docker build -f Dockerfile.hardened -t "yabanin:hardened-${VERSION}" .
# Pin third-party images by digest for reproducibility (record the digests you use):
docker pull pgvector/pgvector:pg16
docker pull redis:7-alpine
mkdir -p dist/offline
docker save "yabanin:hardened-${VERSION}" pgvector/pgvector:pg16 redis:7-alpine \
| gzip > "dist/offline/images-${VERSION}.tar.gz"
Step 3 — Assemble the bundle (connected machine)
VERSION=$(git describe --tags --always)
B="dist/offline/yabanin-offline-${VERSION}"
mkdir -p "${B}/dist" "${B}/compose"
cp -R dist/release "${B}/dist/release"
cp "dist/offline/images-${VERSION}.tar.gz" "${B}/images.tar.gz"
cp deploy/onprem/airgap-compose.yml "${B}/compose/airgap-compose.yml"
cd "$B"
find . -type f ! -name manifest.sha256 -print0 | sort -z \
| xargs -0 sha256sum > manifest.sha256
cd - >/dev/null
tar -czf "dist/offline/yabanin-offline-${VERSION}.tar.gz" -C dist/offline "yabanin-offline-${VERSION}"
Transfer yabanin-offline-<version>.tar.gz to the air-gap machine by approved
media (USB, scp over a private link). Before transfer, confirm the bundle
contains no private material: grep -r "BEGIN PRIVATE" dist/offline/yabanin-offline-*/
must return nothing.
Step 4 — Verify on the air-gap machine
tar -xzf yabanin-offline-<version>.tar.gz
cd yabanin-offline-<version>
- Checksums — tamper check over the whole bundle:
sha256sum -c manifest.sha256
- Signature (only if
SHA256SUMS.sigandyabanin-release.pubare in the bundle):cosign verify-blob --key yabanin-release.pub \--signature dist/release/SHA256SUMS.sig \--insecure-ignore-tlog dist/release/SHA256SUMS--insecure-ignore-tlogskips the transparency-log lookup that would need network (older/newer cosign versions name the offline flag differently — checkcosign verify-blob --help;airgap-verify.shpicks the right flag automatically). - Load images — local operation, no registry:
gunzip -k images.tar.gzdocker load -i images.tar
- Run the image with networking disabled — proves the image itself needs
no connectivity. The unified binary has no
versionsubcommand (onlygateway,admin,loggerd,mcp,migrate,dev,license,completion,help), so use--help:docker run --rm --pull=never --network none "yabanin:hardened-<version>" --help - One command for all of the above:
The script prints onescripts/release/airgap-verify.sh \--artifacts dist/release \--manifest manifest.sha256 \--pubkey yabanin-release.pub \--image-tar images.tar --image "yabanin:hardened-<version>" \--compose compose/airgap-compose.yml
PASS/SKIP/FAILline per stage and exits non-zero on anyFAIL. Skips are honest (e.g. an unsigned bundle reportsSKIP signature, it is not counted as passed). Every network-capable action is either avoided (--pull=never, no curl/wget) or neutralized (--network none).
Step 5 — Minimal offline compose
Do not use deploy/quickstart/docker-compose.yml offline: it contains
build: sections and would trigger builds/pulls. The bundled
compose/airgap-compose.yml (source: deploy/onprem/airgap-compose.yml)
references only pre-loaded images:
name: yabanin-airgap
services:
postgres:
image: pgvector/pgvector:pg16 # loaded from images.tar
environment:
POSTGRES_USER: yabanin
POSTGRES_PASSWORD: replace-me # generate: openssl rand -hex 16
POSTGRES_DB: yabanin
healthcheck:
test: ["CMD-SHELL", "pg_isready -U yabanin -d yabanin"]
interval: 2s
timeout: 5s
retries: 30
redis:
image: redis:7-alpine # loaded from images.tar
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 5s
retries: 30
migrate:
image: yabanin:hardened-<version> # loaded from images.tar
command: ["migrate"]
restart: "no"
environment:
YABANIN_PG_URI: postgres://yabanin:replace-me@postgres:5432/yabanin?sslmode=disable
YABANIN_REGION: onprem
depends_on:
postgres:
condition: service_healthy
gateway:
image: yabanin:hardened-<version>
command: ["gateway"]
environment:
YABANIN_PG_URI: postgres://yabanin:replace-me@postgres:5432/yabanin?sslmode=disable
YABANIN_REGION: onprem
YABANIN_REDIS_ADDR: redis:6379
YABANIN_GATEWAY_LISTEN: ":8082"
ports:
- "127.0.0.1:8082:8082"
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_healthy
Bring it up and check health from the host — the hardened image is distroless and has no shell/curl, so the healthcheck lives on the host:
docker compose -f compose/airgap-compose.yml up -d
curl -fsS http://127.0.0.1:8082/healthz # expect {"status":"ok"}
airgap-verify.sh --compose compose/airgap-compose.yml fails the compose
stage if the file contains build: or references images missing from the
local docker store (i.e. something would be pulled).
Step 6 — Updates
- On the connected machine: bump the code, rebuild artifacts
(
make release-artifacts), rebuild the image with the new version tag, assemble a newyabanin-offline-<new-version>.tar.gz(Steps 1–3). - Transfer, verify (Step 4), then
docker loadthe new images and update theimage:tags incompose/airgap-compose.yml;docker compose up -drecreates the services. - Keep the previous bundle archived for rollback.
Step 7 — Rollback
gunzip -k images-<old-version>.tar.gz && docker load -i images-<old-version>.tar
# point compose image: tags back to yabanin:hardened-<old-version>
docker compose -f compose/airgap-compose.yml up -d
Application rollback is image-level and safe. Database migrations are forward-only; rolling back across a migration requires restoring Postgres from a backup taken before the upgrade.
Limitations (read before signing off an install)
- Without
cosign/COSIGN_KEYat build time the bundle is unsigned;airgap-verify.shreportsSKIP signature. Procurement requirements that mandate provenance need the signed variant. - Without
syftthe SBOM is a per-binarygo version -mbuild manifest, not SPDX JSON. Record this in the delivery notes. - The minimal compose exposes only the gateway on
127.0.0.1:8082and uses placeholder credentials — generate real secrets (openssl rand -hex 16) and follow the full configuration guidance in install-guide.md. yabanin <version>does not exist as a subcommand; the offline run check is--help(see Step 4.4). Adding aversionsubcommand is tracked as a follow-up of RM-52.- Fully automated
build-bundle.sh/install.shpackaging from the original RM-53 card is not implemented here; Steps 1–3 and 4–5 are the manual equivalents andairgap-verify.shautomates the verification half.