---
title: "Air-gapped installation"
---

# Air-gapped installation

Install and run Neural Inverse Govern in a network with no internet access, and how to verify that a release runs fully offline.

Govern (Keystone) is designed to need no internet access at runtime. Everything it needs after installation, from container images to identity providers, mail and model servers, can live inside your network.

Complete offline operation is a property of each release and each installation, not something a bundle proves by existing. Before you rely on it, run the release on a host with no route out, as described in the last section of this page.

## What must be inside the boundary

| Class | Items |
|---|---|
| Images | The Govern images, PostgreSQL, the migration tool, and Mailpit if you use it. The release's `deploy/airgap/images.txt` lists them. |
| Database | PostgreSQL 15 or later and the release's migrations |
| Identity | Your SAML or OIDC identity provider, if you use single sign-on |
| Email | An SMTP relay inside the network |
| Models | Self-hosted model servers, if people use models |
| Platform services | DNS, NTP, your TLS certificate authority, backup, monitoring and a log sink |

Optional integrations (SMTP, LDAP, identity providers, webhooks, vendor audit connectors) create outbound traffic when they are turned on. Point them at services inside your network or leave them off.

## Build the bundle on a connected host

```sh
scripts/airgap/build-bundle.sh --platform linux/amd64 --output dist/ni-auth-airgap
```

The script builds the Govern images, pulls the pinned dependency images listed in `deploy/airgap/images.txt`, saves every image into one archive, and packages the loader scripts, the Docker Compose files and the Helm charts with a `SHA256SUMS` file. For a formal release, replace the image tags in `images.txt` with immutable registry digests.

Move the bundle through your approved media process, then verify `SHA256SUMS` inside the air gap before you load anything.

## Load the images

On each Docker host:

```sh
cd ni-auth-airgap
./install-offline.sh
```

`install-offline.sh` loads the images from the archive without network access. For Kubernetes, either import the images into the container runtime of every schedulable node, or retag and push them to a registry inside your network.

## Run with Docker Compose

Use the air-gap override, which pins every service to a local image and turns pulls off:

```sh
docker compose -f deploy/docker-compose.yml \
  -f deploy/docker-compose.airgap.yml up -d --pull never
```

Run it from the release's source tree, so the migrations and the database role script are present. Set `NI_AUTH_IMAGE`, `NI_AUTH_DASHBOARD_IMAGE`, `POSTGRES_IMAGE`, `MIGRATE_IMAGE` and `MAILPIT_IMAGE` if your imported tags differ from the defaults. Configure the stack as in [Docker Compose](https://neuralinverse.com/docs/deploy/docker-compose/).

## Run on Kubernetes

Install the `keystone` chart as in [Kubernetes](https://neuralinverse.com/docs/deploy/kubernetes/), with these choices for an isolated network:

- `image.registry` set to your internal registry, with a pull secret if it needs one.
- Object storage on a shared ReadWriteMany directory (`objectStorage.local`), or an S3-compatible store inside your network (`objectStorage.s3.endpoint`).
- For the optional telemetry pipeline, a directory cold store (`telemetryPipeline.cold.local`); for Streams tiered storage, `streams.tiering.store=dir`.

Render the chart and check that every image comes from your registry before you install:

```sh
helm template keystone deploy/helm/keystone -f values.yaml \
  | grep -E 'image:|imagePullPolicy:'
```

## Reaching services inside your network

Govern refuses outbound connections to private, loopback, link-local and metadata addresses unless the operator opens them. In an isolated network that is usually exactly where your services are, so list their networks, as narrowly as possible:

| Variable | Opens |
|---|---|
| `NI_AUTH_OUTBOUND_ALLOW_PRIVATE_CIDRS` | Webhooks, approval channels, SIEM collectors, LDAP and Active Directory, inference servers and other tenant-configured destinations |
| `NI_AUTH_MODELS_PRIVATE_NETWORKS` | Model provider connections. Models on `openai_compatible` or `ollama` connections inside these networks count as self-hosted, which is what the `self_hosted` routing rule for restricted data requires. |
| `KEYSTONE_MCP_ALLOW_PRIVATE_NETWORKS` | MCP servers |

Other features that work without reaching outside:

- **Workload identity** for the model gateway accepts an uploaded key set for OIDC issuers Govern cannot reach, such as an air-gapped cluster.
- **Audit tiering** works with an on-premises S3-compatible store that supports Object Lock (for example MinIO), because requests are signed without a cloud SDK.
- **Offline audit verification** with `keystone-audit-verify` needs no network access. See [Verify offline](https://neuralinverse.com/docs/evidence/verify-offline/).

## Offline licence

Offline licences are signed by Neural Inverse with an Ed25519 key. Only the public key is deployed, in one of:

- `NI_AIRGAP_LICENSE_PUBLIC_KEY`: a PEM `PUBLIC KEY` block, or base64 of the 32 raw key bytes;
- `NI_AIRGAP_LICENSE_PUBLIC_KEY_FILE`: a path to a file holding it.

The check fails closed. With neither set, licence registration and reads answer `503 license_verification_unconfigured`. A value that is not a valid Ed25519 public key stops the server. A licence whose signature does not verify answers `400 license_invalid`, and one past its 30-day grace period `422 license_expired`; each refusal is audited as `airgap.license.refused`.

Changes to the air-gap configuration (`POST /api/v1/airgap/license` and the other write routes under `/api/v1/airgap/`) are for the platform operator only; organisation owners and admins get 403. Every change is audited.

The air-gap administration API (`/api/v1/airgap/*`) is partial as a whole: the FIPS mode setting (`PUT /api/v1/airgap/fips`) is stored but nothing else reads it yet, so it does not change how Govern runs. Licence and bundle verification work as described above.

## Verify a release offline

A release is ready for your enclave only when it has run from the transferred bundle alone:

1. Use a disposable host or namespace with no default route and a DNS resolver that refuses every query. Mount no build caches, credential stores or home directories.
2. Verify the bundle's checksums, import the images without registry access, and deploy with pulls disabled.
3. Run the migrations and the startup checks. Exercise sign-in, token issue and refresh, user, organisation and role changes, audit export, restart, and backup and restore, plus each optional integration you plan to use against services inside the network.
4. Capture DNS queries, connection attempts and firewall counters throughout. Any undeclared destination, or any attempt to download a package or image, fails the check. Include a browser, because web assets can reach out on their own.

A strict air gap is enforced by your network controls. These tests are defence in depth, not proof that no network path exists.
