Install air-gapped
Install and run with no route off the host, and verify what you carried across. The offline path is structural, not a setting you enable.
On this page
An air-gapped install of most gateways is a fight against the installer: a chart that pulls subcharts, a base image that wants a registry, a binary that fetches a price table on boot. TokenTriage has none of those, which makes the offline path short.
Two properties do the work. The chart declares zero dependencies, so helm install fetches
nothing a mirror cannot serve. The runtime image is FROM scratch, so there is no
third-party base layer to copy across the gap or to trust once it is there. The pricing table
and the dashboard are compiled into the binary.
Prerequisites: a connected host that can reach both the internet and your internal registry, a way to carry files across, and a private registry on the far side. On Compose you need neither, because the offline profile is self-contained.
Prove it on Compose first
Before planning a transfer, run the shape end to end on one host. The air-gap Compose profile puts the proxy on a network Docker attaches no gateway to, so there is no route off the host at all, so egress is impossible by construction rather than denied by policy, and points it at an in-network mock upstream:
$ docker compose -f docker-compose.yml -f deploy/compose/compose.airgap.yaml \
--profile test up --exit-code-from tests --abort-on-container-exit
[airgap-tests] waiting for /health ...
[airgap-tests] health OK after 0s; driving one completion ...
[airgap-tests] reading the costed row from the admin API ...
[airgap-tests] AIRGAP COSTED-ROW GATE: PASS
That gate drives one completion through the proxy and asserts a costed row comes back —
cost.usd present, priced from the compiled-in table, with no packet able to leave the host and
no API key anywhere in the stack. It is the shortest proof that pricing is not a network call.
What crosses the gap
Three things, and no more:
- The chart.
helm pullthe OCI artifact or the release archive, untarred on the connected side so the whole tree comes across. - The images. Every line of the release’s
images.txt, by digest. - The verification material. The signing public key.
Copy images with cosign copy rather than crane cp, skopeo copy or docker save. Those
three transport the image and drop the signature and attestation sidecar tags, which live as
separate tags in the source repository rather than inside the manifest. An image copied that way
installs fine and cannot be verified afterwards, which is the worst of both outcomes.
Install against your registry
global.imageRegistry rewrites every image reference the chart emits, the product image and
the helm-test client alike, to your mirror prefix. Install from the tree you carried across:
$ helm install tt ./chart/tokentriage \
--namespace tokentriage --create-namespace \
--set global.imageRegistry=<PRIV> \
--set image.digest=sha256:<the digest from images.txt> \
-f ./chart/tokentriage/examples/airgap-values.yaml \
-f my-values.yaml
Three details in that command decide whether it works.
The -f order matters. Later files win, so the preset goes first and your values last.
Pin the digest. A digest is the only reference a registry cannot serve different content
for, and it is what carries a signature check through to what actually runs. The values schema
enforces the sha256: shape, so a typo is refused at render rather than becoming an image-pull
failure behind the gap.
The preset lives inside the chart archive. examples/ is kept in the packaged
archive, so reference it at ./chart/tokentriage/examples/airgap-values.yaml. A repository
path is no use to someone who only has the archive.
The preset itself sets no addresses: it selects the airgap NetworkPolicy preset and states
how to pull. It pins no digest and no registry, because the chart cannot know either.
Then prove it, without a credential:
$ helm test tt -n tokentriage
TEST SUITE: tt-tokentriage-test-connection
Phase: Succeeded
What the air-gap posture actually guarantees
The guarantee is about the install, and it is structural:
- Zero chart dependencies. Every optional backend, whether Redis, Postgres or an L3 cache, is an external reference through a typed values block rather than a bundled subchart, so nothing reaches out mid-install.
- One image,
FROM scratch. No base layer to mirror, pin or trust. The timezone database is knowingly absent; budgets and windows are UTC by contract, so nothing in the runtime reads local time. Do not add a base image to “fix” that. - Credentials by reference only. The chart holds no key; you create the
Secret.
At runtime, every outbound tier is gated on the binary’s own consent string. Shared state, the
L3 cache tier, an external embeddings API, OTLP export, a guardrail judge, the guardrail
sidecar and the evals-plane LLM judge that grades quality are each refused unless their config
block carries external: "allow", a string you type, that no deployment layer types for you. A
default install dials nothing. The judge gate is unconditional in one way worth knowing before
you plan around it: it does not read the endpoint, so pointing the grader at a listener on your
own box still needs the opt-in typed.
Air-gap judging, and the known limits is that tier in full.
Verify what you downloaded
Every release is signed twice on the same image, for two different audiences.
The key-pair signature is the air-gap path. The pipeline signs with a static key and no transparency-log upload, so verification behind the gap needs nothing but the public key:
$ cosign verify --key cosign.pub --offline --insecure-ignore-tlog=true \
<PRIV>/tokentriage/tokentriage@sha256:<digest>
The keyless signature is for connected consumers. It is an online operation on both sides, which is precisely why it is not the offline path, and it exists so anyone can verify without a pre-shared key.
Each release also carries an SPDX software bill of materials and build provenance as
attestations on the image, and images.txt as the machine-readable mirror list.
The third-party images in that list, the busybox helper and the development backends, are not
signed by this project. Do not run cosign verify against them; it would correctly fail for
want of a signature. Their integrity is the digest pin: a registry cannot serve content whose
digest differs.
One limitation of the whole approach is worth planning around. A public key carried across a gap has no revocation feed. Nothing behind the gap can learn that a key was rotated or a root revoked, so the key you carried is a snapshot with an expiry rather than a standing authority. Schedule a re-transfer on a fixed cadence, and record that cadence in your change management, because the artifact cannot enforce it for you.
What this project has not executed, and what that means for the two sections above
The transfer and verification procedures above are documented from the release pipeline and the supply-chain design. They have not been executed against a published release, because no release has been published yet: there is no tagged version, no OCI chart in a registry, and no signed image to pull. The pipeline exists and has never run.
What has been executed and is quoted verbatim on this page: the Compose air-gap gate, the
rendered NetworkPolicy, and a chart install against a locally mirrored image with a pinned
reference. The offline verification loop is reproducible today with a throwaway key pair and a
local registry: sign an image with --tlog-upload=false and verify it with --offline. That
is how the design was proven, and which is not the same claim as verifying a real release.
Treat the two sections above as the procedure to follow at the first release rather than as a report of one.
Guardrail consent is necessary but does not enable unsupported capabilities. Stock sidecar activation remains rejected pending runtime/model qualification; positive judge hard caps require a proven reservation bound. See the current activation table.
What this does not cover
The guardrails tier documents its own offline story, including which detectors are embedded and which leave the process, on air-gap & known limits. The consent gate’s effect on caching and governance belongs to those sections. Hardening the install you have made offline is production hardening.