Air-gap deployment

Air-gapped installs run Control Core entirely inside a disconnected or sovereign enclave. Policy sync, image pulls, breach checks, and observability must stay inside your boundary — no dependency on github.com, public CDNs, or foreign SaaS telemetry.

This guide complements Air-gapped license installation (offline license verification) with runtime posture for the Control Plane, Policy Bridge, and Bouncers.

TL;DR checklist

StepAction
1Pre-pull and verify signed images (scripts/pre-pull-images.sh)
2Set DEMO_AIR_GAP=true and point POLICY_REPO_URL at internal Gitea — never github.com
3Leave LLM provider API keys unset unless approved egress exists
4Set BREACH_CHECK_DISABLED=true (or supply local HIBP range file) — support guidance required
5Enable Helm networkPolicy.enabled: true for default-deny east-west
6Confirm TELEMETRY_ENABLED=false and bundled fonts (no Google Fonts CDN)

1. Pre-pull container images (~15 min)

On a bastion with one-time registry access, stage digest-pinned images into your private registry:

# From the customer release bundle (controlcore2026 package)
bash scripts/pre-pull-images.sh \
  --registry registry.internal.example/controlcore \
  --manifest SBOM-v2026.01.json

The script pulls each controlcoreio/* image listed in the release SBOM, re-tags for your registry, and prints @sha256: digests for docker-compose.yml or Helm values.

Troubleshooting: If Cosign verify fails, confirm you imported the release public key from the bundle. Do not deploy unsigned images. See Supply chain verification.

Transfer images to the air-gap enclave via approved media or one-way sync, then update global.imageRegistry in Helm or digest placeholders in Compose.

2. Policy repo — internal Gitea (~10 min)

Policy Bridge syncs controls from Git. In air-gap, never point at github.com.

DEMO_AIR_GAP=true
POLICY_REPO_URL=https://gitea.internal.example/controlcore/policies.git
OPAL_POLICY_REPO_URL=https://gitea.internal.example/controlcore/policies.git

When DEMO_AIR_GAP=true and the repo URL still contains github.com, Control Plane API disables Policy Bridge autostart at boot and logs a critical error — this prevents silent failure to sync.

Optional: enable the in-cluster Gitea subchart for lab installs:

helm upgrade --install controlcore ./controlcore \
  -f values.yaml \
  -f values-offline.yaml \
  --set gitea.enabled=true

Seed the internal repo with your controls (export from a staging Control Plane or curated bundle), then configure Policy Bridge credentials via Kubernetes Secret / ExternalSecrets.

Troubleshooting: If policies never appear on Bouncers, verify Gitea reachability from Control Plane API, webhook/poll interval, and GET Policy Bridge health endpoint on the Control Plane. Full reference: /troubleshooting.

3. Breach check posture

Password breach lookup uses HIBP range API by default. Air-gap installs should either:

  • Set BREACH_CHECK_DISABLED=true for isolated lab (document the risk), or

Warning: Set BREACH_CHECK_DISABLED=true only under guidance from Control Core support (support@controlcore.io).

  • Mount a local breach-range file and configure the offline path per Password policy

Production defence enclaves typically disable outbound HIBP and rely on local policy.

4. No outbound LLM keys by default

Leave upstream LLM provider API keys unset in AI Gateway backends unless your security review approves egress to a specific provider endpoint inside the enclave. AI Gateway routing CRDs can target on-prem OpenAI-compatible endpoints without public internet.

Controls and audit still function; only external model calls require explicit credential configuration.

5. Self-hosted fonts and zero CDN egress

The Control Plane UI bundles Inter via @fontsource-variable/inter — no Google Fonts CDN at runtime. Verify your build matches the signed release; do not re-enable remote font imports in custom themes.

Set on Control Plane and optional Langfuse:

TELEMETRY_ENABLED=false
LANGFUSE_TELEMETRY_ENABLED=false

Helm sovereignty overlay:

global:
  zeroForeignEgress: true
langfuse:
  enabled: false   # opt-in only; keep in-cluster if enabled
  environment:
    TELEMETRY_ENABLED: "false"
    LANGFUSE_TELEMETRY_ENABLED: "false"

6. Network policy (Helm air-gap overlay)

Enable default-deny NetworkPolicies so only required east-west paths (Control Plane ↔ Postgres, Bouncer ↔ policy engine, Policy Bridge ↔ Gitea) are allowed:

# values-air-gap.yaml (customer overlay)
networkPolicy:
  enabled: true
  ingressControllerNamespace: ingress-nginx
  papApiExtraEgress: []      # add SAML IdP, KMS, internal Postgres if external
  bouncerExtraEgress: []       # add protected upstream CIDRs as needed

Requires a CNI that enforces NetworkPolicy (Calico, Cilium, AWS VPC CNI with policy).

Troubleshooting: If Bouncers cannot reach the policy engine or upstream after enabling policies, inspect denied connections with your CNI flow logs and append targeted rules to bouncerExtraEgress. Start with networkPolicy.enabled: false, validate baseline, then enable.

Verification (~5 min)

# Control Plane healthy
curl -fsS "$CONTROL_PLANE/health/live"

# Policy Bridge — no pending stuck syncs
curl -fsS -H "Authorization: Bearer $TOKEN" "$CONTROL_PLANEPolicy Bridge health endpoint" | jq .

# Confirm no github.com in policy repo env (from Control Plane API pod)
kubectl exec deploy/Control Plane API -- printenv POLICY_REPO_URL

Expect internal Gitea URL, DEMO_AIR_GAP=true, and Policy Bridge status: healthy after first successful sync.

Next steps