Skip to main content

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​

FileProduced byPurpose
dist/release/yabanin-*make release-artifactsstatic service binaries (-trimpath, stamped version/commit)
dist/release/SHA256SUMSmake release-artifactssha256 over all binaries
dist/release/SHA256SUMS.sigmake release-artifacts (only when COSIGN_KEY is set)cosign signature over SHA256SUMS
dist/release/sbom.spdx.json or dist/release/*.buildinfomake release-artifactsSBOM (syft SPDX), or go version -m build manifests as fallback when syft is not installed
images.tar.gzdocker savehardened Yabanin image + pgvector/pgvector:pg16 + redis:7-alpine
manifest.sha256sha256sumchecksums over every file in the bundle (tamper check before install)
yabanin-release.pubcosign public-keypublic key matching COSIGN_KEY (public material only)
compose/airgap-compose.ymldeploy/onprem/airgap-compose.ymlminimal 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:

  • go and make (for make release-artifacts);
  • docker (image build + docker save);
  • cosign — optional but required for a signed bundle; if not installed, make release-artifacts skips signing and the release must be marked unsigned in the release notes;
  • syft — optional; if not installed, the SBOM falls back to go version -m build manifests (.buildinfo per 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>
  1. Checksums — tamper check over the whole bundle:
    sha256sum -c manifest.sha256
  2. Signature (only if SHA256SUMS.sig and yabanin-release.pub are in the bundle):
    cosign verify-blob --key yabanin-release.pub \
    --signature dist/release/SHA256SUMS.sig \
    --insecure-ignore-tlog dist/release/SHA256SUMS
    --insecure-ignore-tlog skips the transparency-log lookup that would need network (older/newer cosign versions name the offline flag differently — check cosign verify-blob --help; airgap-verify.sh picks the right flag automatically).
  3. Load images — local operation, no registry:
    gunzip -k images.tar.gz
    docker load -i images.tar
  4. Run the image with networking disabled — proves the image itself needs no connectivity. The unified binary has no version subcommand (only gateway, admin, loggerd, mcp, migrate, dev, license, completion, help), so use --help:
    docker run --rm --pull=never --network none "yabanin:hardened-<version>" --help
  5. One command for all of the above:
    scripts/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
    The script prints one PASS/SKIP/FAIL line per stage and exits non-zero on any FAIL. Skips are honest (e.g. an unsigned bundle reports SKIP 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​

  1. On the connected machine: bump the code, rebuild artifacts (make release-artifacts), rebuild the image with the new version tag, assemble a new yabanin-offline-<new-version>.tar.gz (Steps 1–3).
  2. Transfer, verify (Step 4), then docker load the new images and update the image: tags in compose/airgap-compose.yml; docker compose up -d recreates the services.
  3. 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_KEY at build time the bundle is unsigned; airgap-verify.sh reports SKIP signature. Procurement requirements that mandate provenance need the signed variant.
  • Without syft the SBOM is a per-binary go version -m build manifest, not SPDX JSON. Record this in the delivery notes.
  • The minimal compose exposes only the gateway on 127.0.0.1:8082 and 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 a version subcommand is tracked as a follow-up of RM-52.
  • Fully automated build-bundle.sh / install.sh packaging from the original RM-53 card is not implemented here; Steps 1–3 and 4–5 are the manual equivalents and airgap-verify.sh automates the verification half.