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
| Step | Action |
|---|---|
| 1 | Pre-pull and verify signed images (scripts/pre-pull-images.sh) |
| 2 | Set DEMO_AIR_GAP=true and point POLICY_REPO_URL at internal Gitea — never github.com |
| 3 | Leave LLM provider API keys unset unless approved egress exists |
| 4 | Set BREACH_CHECK_DISABLED=true (or supply local HIBP range file) — support guidance required |
| 5 | Enable Helm networkPolicy.enabled: true for default-deny east-west |
| 6 | Confirm 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, andGET Policy Bridge health endpointon 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=truefor isolated lab (document the risk), or
Warning: Set
BREACH_CHECK_DISABLED=trueonly 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 withnetworkPolicy.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
- Custom (Kubernetes / Helm) — production hardening
- Kickstart (Docker Compose) — lab Compose with
.envtemplate - Supply chain verification — Cosign + SBOM
- Langfuse setup — optional in-cluster LLM tracing
- Air-gapped license installation