# Air-gapped install

> Install self-hosted MergeHarbor in a network with no internet access, from a checksummed image bundle, with models served inside your network.

MergeHarbor can run in a network with no internet access. Every release has an air-gap bundle with all images. The license is checked offline, and MergeHarbor sends no telemetry and makes no update checks.

## 1. Get the bundle

Each release `vX.Y.Z` has two bundles: `mergeharbor-airgap-X.Y.Z-amd64.tar` and `mergeharbor-airgap-X.Y.Z-arm64.tar`, each with a `.sha256` file. MergeHarbor gives licensed customers access to them. Each bundle contains:

| Path                          | Contents                                                                                                                                                                                               |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `images/`                     | Every image as an OCI layout, digests preserved: api, worker, agent, embedder (with its embedding model), console, mergeharbor (CLI), sandbox-runner, plus PostgreSQL and Caddy for the Compose bundle |
| `images.txt`, `manifest.json` | Name, tag, digest, platform and source of each image                                                                                                                                                   |
| `chart/`                      | The Helm chart package                                                                                                                                                                                 |
| `self-hosted/`                | The Docker Compose bundle                                                                                                                                                                              |
| `load-bundle.sh`              | Verifies the bundle and loads the images                                                                                                                                                               |
| `SHA256SUMS`                  | Checksums of every file                                                                                                                                                                                |

Images are signed with Sigstore cosign and ship with an SBOM, so you can check them with `cosign verify` before you bring them in.

## 2. Load the images

Copy the `.tar` and `.sha256` files into the network. `load-bundle.sh` checks both checksum files before it loads anything. To check the bundle without loading it, add `--verify-only`.

**Into a private registry** (for Kubernetes). Sign in to the registry first, with `skopeo login`, `crane auth login` or `docker login`:

```sh
tar -xf mergeharbor-airgap-1.4.0-amd64.tar mergeharbor-airgap-1.4.0-amd64/load-bundle.sh
./mergeharbor-airgap-1.4.0-amd64/load-bundle.sh --registry registry.internal:5000/mergeharbor \
  mergeharbor-airgap-1.4.0-amd64.tar
```

- It uses skopeo, crane or Docker, whichever it finds; skopeo and crane keep the image digests.
- Add `--insecure` for a registry with plain HTTP or a self-signed certificate.
- It prints the Helm values to use.

**Into the local Docker** (for the single-VM Compose install, no registry needed):

```sh
./mergeharbor-airgap-1.4.0-amd64/load-bundle.sh --docker-local mergeharbor-airgap-1.4.0-amd64.tar
```

## 3. Point models at your network

With `AIR_GAPPED=true`, the review agent only creates in-network providers. It refuses to start if a provider's endpoint resolves to a public address.

| Models                                                          | Settings                                                                                                                                                                                                                                                                             |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Your own server: vLLM, Ollama, TGI or any OpenAI-compatible API | `SELFHOSTED_LLM_BASE_URL` (Helm: `agent.selfHostedLLM.baseUrl`), plus a priced model entry (Compose: `config/models.yaml`; Helm: `agent.modelRegistry`)                                                                                                                              |
| Amazon Bedrock, through VPC endpoints                           | `AWS_REGION`. For Claude models: `ANTHROPIC_BEDROCK_MANTLE_BASE_URL` at a private endpoint (Helm: `agent.bedrockMantleUrl`). For Converse models: a `bedrock-runtime` interface endpoint with private DNS, or `AWS_ENDPOINT_URL_BEDROCK_RUNTIME` (Helm: `agent.bedrockEndpointUrl`). |

Open-weight models served by vLLM or Ollama are the tested air-gapped setup. Claude through Amazon Bedrock in an air-gapped network is not confirmed yet.

Set `MODEL_PROVIDERS` to the providers you configured, for example `openai_compatible`. When air-gapped, routing only uses `openai_compatible`, `bedrock` and `bedrock_converse`.

Embeddings must come from inside the network too:

- **Compose:** the bundled embeddings server has its model inside the image. Nothing to set.
- **Kubernetes:** set `embedder.enabled: true` to run the same server in the cluster, or point `config.EMBED_URL` at your own embeddings server.

## 4. Install

**Helm:**

```yaml
image:
  registry: registry.internal:5000/mergeharbor
  pullSecrets: [{ name: regcred }]
airGapped: true
config:
  MODEL_PROVIDERS: openai_compatible
embedder:
  enabled: true
agent:
  selfHostedLLM: { baseUrl: http://vllm.models.svc:8000/v1 }
  reviewModel: selfhosted/qwen-coder
  summaryModel: selfhosted/qwen-coder
```

```sh
helm install mergeharbor ./mergeharbor-airgap-1.4.0-amd64/chart/mergeharbor-1.4.0.tgz -n mergeharbor -f values.yaml
```

**Compose:** in the bundle's `self-hosted/` directory, set `AIR_GAPPED=true` and `TLS_MODE=internal` (or `files`) in `.env`. Then run `./install.sh --offline --domain … --version 1.4.0`.

## 5. Load the vulnerability database

Dependency checks, SBOMs and vulnerability management match against the vulnerability database, which the vulnerability service (the `vuln` container) keeps in its own database. An air-gapped installation can't download the advisory feeds, so you copy the database in as a file. With `AIR_GAPPED=true`, feed sync and the public OSV API are off automatically. Until you import a file, dependency checks find no vulnerabilities, and public advisory pages show only what you imported.

**Export on a connected machine.** A connected MergeHarbor keeps advisories only for the packages its organizations use, so export a **full** bundle, read straight from the public advisory exports. Run it in that installation's vulnerability service:

```sh
vuln kb export -mode full -o vulndb-full.jsonl.gz
# distributions and container images too, if you import SBOMs of images:
vuln kb export -mode full -with-os Debian,Ubuntu,Alpine -o vulndb-full.jsonl.gz
```

The file is gzip-compressed JSON lines. It holds every advisory record of the code ecosystems (and the distributions you name), CISA's Known Exploited Vulnerabilities catalog, EPSS scores, the CWE list and the SPDX license list.

**Move the file** into the network the same way you brought in the image bundle.

**Import it** inside the network (the service needs no internet access for this):

- **Compose:** put the file in the `vulndb/` directory next to `compose.yml`, then run:

  ```sh
  ./compose.sh exec vuln app kb import -file /vulndb/vulndb-full.jsonl.gz
  ```

- **Helm:** stream the file into the vulnerability service's pod:

  ```sh
  kubectl -n mergeharbor exec -i deploy/mergeharbor-vuln -- /usr/local/bin/app kb import -file /dev/stdin < vulndb-full.jsonl.gz
  ```

The import prints how many records it loaded. You can run it while MergeHarbor is running.

**Refresh it regularly.** New advisories, fixes and exploit data appear every day. Export and import a new file at least weekly, or daily if you can. Each import updates the database in place, so a newer file simply goes over the older one, and an interrupted import can be run again.

What you get: advisories for npm, PyPI, Go, Maven, NuGet, crates.io, RubyGems, Packagist, Pub and Hex as of the bundle's date, plus the distributions you asked for with `-with-os`; CISA KEV, EPSS, CWE and SPDX data as of the exporting installation's last sync. You don't get package health (OpenSSF Scorecard, release activity, end of life) for packages the exporting installation didn't use, or distribution and image coverage you didn't ask for. `-mode wanted` exports only the exporting installation's own packages (plus `-top-n N` popular packages per ecosystem), which suits a second installation of the same organization.

## What AIR_GAPPED=true does

- **Egress guard.** It refuses every public address for all outbound connections MergeHarbor makes on your behalf: code hosts, identity providers, connectors, webhooks, audit log streaming, notifications and MCP servers. Private addresses are allowed; cloud metadata addresses stay blocked.
- **Vulnerability database.** Feed sync (`VULNDB_SYNC`), lookups against the public OSV API (`OSV_API_URL`) and package lookups (`VULNDB_DEPSDEV_URL`, `VULNDB_EOL_URL`, `VULNDB_PACKAGIST_URL`) are off. MergeHarbor refuses to start if you turn any of them on. Repositories without lockfiles are read from the versions written in their manifests only (transitive dependencies aren't estimated); set `VULNDB_MAVEN_URL` to an in-network Maven mirror so parent and BOM POMs outside a repository can be read. Load the database from a file instead (step 5).
- **Startup check.** MergeHarbor refuses to start when a feature that only works with public services is configured: card payments, the website bot check, the public review demo, the Slack, Microsoft Teams, Google Chat and Discord apps, and Google or Microsoft sign-in.
- **Review agent.** It never calls the public Anthropic, OpenAI or Gemini APIs.

Use your own identity provider over OpenID Connect or SAML, and an internal SMTP relay. With the egress network policy on, list their private ranges in `networkPolicy.egress.allowCIDRs`.
