Skip to content

Air-Gap Deployment

Air-gap mode deploys the Arbitex Outpost in an environment with no network access to the Arbitex Platform management plane. All configuration — policy bundles, DLP models, and GeoIP databases — is provisioned via local filesystem volumes rather than downloaded at runtime.

Air-gap deployments are used for:

  • Classified or air-gapped government networks
  • Healthcare environments with strict outbound network controls
  • Financial institutions with network segmentation requirements
  • Environments where SEC 17a-4 WORM compliance requires isolation from external systems

Set the OUTPOST_AIRGAP environment variable to true:

Terminal window
OUTPOST_AIRGAP=true

When air-gap mode is enabled at startup, the Outpost:

  1. Skips mTLS certificate validation (no Platform connectivity to validate against)
  2. Skips cert bundle auto-download
  3. Does not start the background policy sync task
  4. Does not start the heartbeat sender
  5. Does not start the cert rotation scheduler
  6. Loads all configuration from local filesystem volumes

All five disabled capabilities have local-volume equivalents described in this guide.


Terminal window
OUTPOST_AIRGAP=true
AIRGAP_POLICY_PATH=/opt/arbitex/policies # default

The Outpost loads policy_bundle.json from $AIRGAP_POLICY_PATH/policy_bundle.json at startup.

The policy bundle is a JSON document containing:

  • DLP rules (all active rules from the compliance and custom policy configuration)
  • OAuth scope enforcement configuration
  • Model access controls and risk tier assignments

To generate a policy bundle from the Platform:

Terminal window
# Export current active policy bundle from Platform admin API
curl https://your-platform/api/v1/admin/policy-bundle/export \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-o policy_bundle.json
deployment.yaml
env:
- name: OUTPOST_AIRGAP
value: "true"
- name: AIRGAP_POLICY_PATH
value: /opt/arbitex/policies
volumeMounts:
- name: policy-bundle
mountPath: /opt/arbitex/policies
readOnly: true
volumes:
- name: policy-bundle
configMap:
name: arbitex-policy-bundle

Create the ConfigMap from your exported bundle:

Terminal window
kubectl create configmap arbitex-policy-bundle \
--from-file=policy_bundle.json=./policy_bundle.json \
-n arbitex

Since there is no background sync, policy bundle updates require a rolling restart:

  1. Export a new policy_bundle.json from the Platform (or prepare manually).
  2. Update the ConfigMap or volume source.
  3. Perform a rolling restart of the Outpost deployment.

The Outpost logs the bundle version at startup: air_gap=True policy_bundle version=<version>.


The DeBERTa v3 ONNX model provides Tier 3 contextual DLP classification. In air-gap mode, the model is loaded from a local volume rather than downloaded.

Terminal window
OUTPOST_AIRGAP=true
AIRGAP_MODEL_PATH=/opt/arbitex/models # default
# Alternatively, override directly:
DEBERTA_MODEL_PATH=/opt/arbitex/models/model.onnx

At startup, the Outpost resolves the model path in this order:

  1. If DEBERTA_MODEL_PATH is explicitly set and the file exists, use it directly.
  2. Otherwise, look for model.onnx in $AIRGAP_MODEL_PATH/model.onnx.
  3. If neither is found, Tier 3 classification is disabled (Tier 1 and Tier 2 DLP remain active).

The model file (model.onnx) is large (~500 MB). Use a PersistentVolume rather than a ConfigMap:

deployment.yaml
env:
- name: AIRGAP_MODEL_PATH
value: /opt/arbitex/models
volumeMounts:
- name: deberta-model
mountPath: /opt/arbitex/models
readOnly: true
volumes:
- name: deberta-model
persistentVolumeClaim:
claimName: arbitex-deberta-model-pvc

Pre-populate the PVC with the promoted model artifact. The Outpost uses either optimum.onnxruntime (preferred, if available in the container image) or raw onnxruntime.InferenceSession as a fallback loader.

The model artifact used in air-gap deployments should match the version shipped with the Outpost container image. Check the model version in the startup logs:

DeBERTa Tier3 scanner loaded: model.onnx (label_map={0: 'pii', 1: 'clean'}, threshold=0.7)

The Outpost uses GeoIP enrichment to annotate audit log entries with country, city, and ISP data from the request IP address. In air-gap mode, the MMDB database is provisioned via volume or baked into the container image.

The Outpost resolves the GeoIP database in this order:

  1. Explicit path (GEOIP_MMDB_PATH): if set and the file exists, use it.
  2. Download on start (GEOIP_DOWNLOAD_ON_START=true): skipped in air-gap mode — the download step is bypassed entirely when OUTPOST_AIRGAP=true.
  3. Bundled fallback (GEOIP_MMDB_FALLBACK_PATH): a pre-baked MMDB baked into the container image or mounted separately. Used when steps 1 and 2 are unavailable.
Terminal window
# Option A: explicit volume mount
GEOIP_MMDB_PATH=/opt/arbitex/geoip/GeoLite2-City.mmdb
# Option B: use baked-in fallback
GEOIP_MMDB_FALLBACK_PATH=/app/geoip/GeoLite2-City.mmdb

If neither path resolves to a valid file, GeoIP enrichment is disabled and audit records omit geographic fields. This is a graceful degradation — the Outpost continues operating without GeoIP.

env:
- name: GEOIP_MMDB_PATH
value: /opt/arbitex/geoip/GeoLite2-City.mmdb
volumeMounts:
- name: geoip-db
mountPath: /opt/arbitex/geoip
readOnly: true
volumes:
- name: geoip-db
configMap:
name: arbitex-geoip
Terminal window
kubectl create configmap arbitex-geoip \
--from-file=GeoLite2-City.mmdb=./GeoLite2-City.mmdb \
-n arbitex

Note: MaxMind GeoLite2 databases require a free MaxMind account to download. The anonymous-IP database (GEOIP_ANON_DB_PATH) requires a commercial MaxMind subscription.


When the Outpost loses connectivity to the Platform audit sync endpoint — or in air-gap mode where no connectivity exists — audit events are written to a local SQLite queue rather than dropped.

Terminal window
AUDIT_QUEUE_DB_PATH=audit_queue/audit_queue.db # default

The SQLite database file is created automatically at the configured path. In Kubernetes, mount a PersistentVolume at this path to ensure queue durability across pod restarts.

Condition Behavior
Platform reachable (circuit CLOSED) Audit events sent directly to Platform; SQLite queue drained asynchronously
Platform unreachable (circuit OPEN) Audit events written to SQLite queue; no data loss
Circuit transitions to CLOSED Flush task drains queue in 50-event batches to Platform

The queue stores events as JSON in the local_audit_queue table:

CREATE TABLE local_audit_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_json TEXT NOT NULL,
queued_at REAL NOT NULL,
retry_count INTEGER NOT NULL DEFAULT 0
);

When connectivity is restored (audit circuit breaker closes), the Outpost automatically flushes the SQLite queue:

  1. _sync_all_pending() detects local_queue.count() > 0 and circuit state CLOSED.
  2. _flush_local_queue() sends batches of 50 events to POST /v1/internal/outpost-audit-sync.
  3. Successfully delivered events are deleted (mark_synced(ids)).
  4. Failed events increment their retry_count for next flush attempt.

There is no manual flush required. The flush runs automatically on each sync cycle once the circuit closes.

Check queue depth via the Outpost admin API:

Terminal window
curl http://localhost:8080/admin/api/sync-status \
-H "Authorization: Bearer $OUTPOST_ADMIN_TOKEN"

Response includes audit_queue_depth — the number of events pending flush. A persistently non-zero queue depth in non-air-gap mode indicates connectivity issues with the Platform audit endpoint.

In a permanent air-gap deployment (no connectivity ever), the SQLite queue accumulates all audit events indefinitely. To export audit events from a permanently air-gapped Outpost:

  1. Access the SQLite database file directly (volume mount or kubectl exec).
  2. Extract events: sqlite3 audit_queue.db "SELECT event_json FROM local_audit_queue;"
  3. Transfer the extracted events to the Platform via a secure offline transfer process.

Standard Cert Rotation (disabled in air-gap)

Section titled “Standard Cert Rotation (disabled in air-gap)”

In a connected deployment, the Outpost automatically renews its mTLS certificate 30 days before expiry by calling the Platform cert renewal endpoint. This scheduler is disabled in air-gap mode:

Air-gap mode: cert rotation disabled (no platform connectivity)

Manual Cert Rotation for Air-Gap Deployments

Section titled “Manual Cert Rotation for Air-Gap Deployments”

In air-gap mode, operators are responsible for certificate rotation:

  1. Monitor cert expiry: The /admin/api/sync-status endpoint returns cert_expiry_days.
  2. Generate replacement certs: Provision a new cert/key pair using your PKI (step-ca, internal CA, or manual).
  3. Update volume mounts: Replace the cert and key files on the mounted volume.
  4. Rolling restart: Restart the Outpost pods to load the new certificates.
Terminal window
# Check cert expiry from the Outpost admin API
curl http://localhost:8080/admin/api/sync-status \
-H "Authorization: Bearer $OUTPOST_ADMIN_TOKEN" | jq '.cert_expiry_days'

Set up monitoring alerts when cert_expiry_days drops below your rotation lead time (recommend: 45 days for air-gap environments where restarts require scheduling).

In air-gap mode, CERT_BUNDLE_AUTO_DOWNLOAD is implicitly disabled. Configure the cert bundle path directly:

Terminal window
OUTPOST_TLS_CERT=/opt/arbitex/certs/outpost.crt
OUTPOST_TLS_KEY=<path-to-outpost-key>
OUTPOST_CA_BUNDLE=/opt/arbitex/certs/ca-bundle.crt

Terminal window
# === Core air-gap switch ===
OUTPOST_AIRGAP=true
# === Policy bundle ===
AIRGAP_POLICY_PATH=/opt/arbitex/policies # contains policy_bundle.json
# === DeBERTa model ===
AIRGAP_MODEL_PATH=/opt/arbitex/models # contains model.onnx
# or: DEBERTA_MODEL_PATH=/opt/arbitex/models/model.onnx
# === GeoIP ===
GEOIP_MMDB_PATH=/opt/arbitex/geoip/GeoLite2-City.mmdb
# or: GEOIP_MMDB_FALLBACK_PATH=/app/geoip/GeoLite2-City.mmdb
# === Audit queue ===
AUDIT_QUEUE_DB_PATH=/opt/arbitex/audit/audit_queue.db
# === Certificates ===
OUTPOST_TLS_CERT=/opt/arbitex/certs/outpost.crt
OUTPOST_TLS_KEY=<path-to-outpost-key>
OUTPOST_CA_BUNDLE=/opt/arbitex/certs/ca-bundle.crt
# === DLP settings ===
DLP_DEBERTA_ENABLED=true # enable if model is provisioned
DEBERTA_MODEL_PATH=/opt/arbitex/models/model.onnx

apiVersion: apps/v1
kind: Deployment
metadata:
name: arbitex-outpost-airgap
namespace: arbitex
spec:
replicas: 2
selector:
matchLabels:
app: arbitex-outpost
template:
metadata:
labels:
app: arbitex-outpost
spec:
containers:
- name: outpost
image: arbitex/outpost:latest
env:
- name: OUTPOST_AIRGAP
value: "true"
- name: AIRGAP_POLICY_PATH
value: /opt/arbitex/policies
- name: AIRGAP_MODEL_PATH
value: /opt/arbitex/models
- name: GEOIP_MMDB_PATH
value: /opt/arbitex/geoip/GeoLite2-City.mmdb
- name: AUDIT_QUEUE_DB_PATH
value: /opt/arbitex/audit/audit_queue.db
- name: DLP_DEBERTA_ENABLED
value: "true"
volumeMounts:
- name: policy-bundle
mountPath: /opt/arbitex/policies
readOnly: true
- name: deberta-model
mountPath: /opt/arbitex/models
readOnly: true
- name: geoip-db
mountPath: /opt/arbitex/geoip
readOnly: true
- name: audit-queue
mountPath: /opt/arbitex/audit
- name: tls-certs
mountPath: /opt/arbitex/certs
readOnly: true
volumes:
- name: policy-bundle
configMap:
name: arbitex-policy-bundle
- name: deberta-model
persistentVolumeClaim:
claimName: arbitex-deberta-model-pvc
- name: geoip-db
configMap:
name: arbitex-geoip
- name: audit-queue
persistentVolumeClaim:
claimName: arbitex-audit-queue-pvc
- name: tls-certs
secret:
secretName: arbitex-outpost-tls

At startup, the Outpost logs its air-gap configuration summary:

INFO Air-gap mode enabled
INFO Policy bundle loaded: version=2026-03-13, rules=47
INFO DeBERTa Tier3 scanner loaded: model.onnx (threshold=0.7)
INFO GeoIP: city database loaded (path=/opt/arbitex/geoip/GeoLite2-City.mmdb)
INFO Audit queue: SQLite backend at /opt/arbitex/audit/audit_queue.db
INFO Air-gap mode: cert rotation disabled (no platform connectivity)
INFO Air-gap mode: heartbeat disabled (no platform connectivity)
INFO Air-gap mode: policy sync disabled (no platform connectivity)

If any required volume is missing or the policy bundle fails to parse, the Outpost logs an error and continues with the subsystem disabled (graceful degradation) — it does not abort startup.


Feature Air-Gap Support Notes
DLP scanning (Tier 1 + Tier 2) Full Pattern matching and compliance bundles
DLP scanning (Tier 3 DeBERTa) Full Requires model volume mount
GeoIP enrichment Full Requires MMDB volume mount
Audit logging Full Events stored in SQLite queue
Policy enforcement Full Loaded from policy_bundle.json
mTLS cert rotation Manual only No automatic rotation
Platform heartbeat Disabled No connectivity to Platform
CredInt breach lookup Disabled No connectivity to CredInt microservice
Policy bundle sync Manual only Rolling restart required for updates

Understanding what requires external connectivity versus what operates fully offline prevents configuration surprises when bringing an air-gapped Outpost online.

No connectivity required (fully air-gapped)

Section titled “No connectivity required (fully air-gapped)”

These capabilities load from local volumes and require zero egress:

Capability Source
DLP scanning (all tiers) Local volume mounts
Policy enforcement policy_bundle.json on local volume
GeoIP enrichment MMDB file on local volume
Audit logging SQLite queue — no egress needed
Prometheus /metrics scrape Always available, pull-only
Pushgateway metrics push Works to an internal Pushgateway
OTel trace export Works to an internal OTel Collector
SIEM direct sink Works to internal syslog or Splunk HEC
Local audit log export Writes to a local mounted volume

Requires platform connectivity (disabled in air-gap)

Section titled “Requires platform connectivity (disabled in air-gap)”

These capabilities are disabled when OUTPOST_AIRGAP=true and cannot be re-enabled without platform network access:

  • Background policy sync (runs every 60 seconds in connected mode)
  • Heartbeat sender
  • Cert rotation scheduler
  • Cert bundle auto-download
  • GeoIP MMDB download on start
  • CredInt CDN bloom filter refresh
  • Audit sync to Platform
  • Platform management URL validation

These services are disabled by default. When your air-gapped network includes them, you can opt in with the corresponding config keys:

Service Purpose Config key
Prometheus server Scrapes /metrics endpoint None — always on
Pushgateway Receives metrics push PUSHGATEWAY_ENABLED=true
OTel Collector Receives trace/span export OTEL_ENABLED=true
Syslog server or Splunk HEC Receives audit export SIEM_DIRECT_ENABLED=true

Online update flow (unavailable in air-gap)

Section titled “Online update flow (unavailable in air-gap)”

In a connected deployment, the admin API drives updates automatically:

Terminal window
# Check for new release (requires SOFTWARE_UPDATE_RELEASE_URL)
POST /admin/updates/check
# Download and stage bundle + .sig, verify Ed25519 or HMAC-SHA256
POST /admin/updates/download

Setting SOFTWARE_UPDATE_RELEASE_URL="" (empty string) disables the online check. This is the correct setting for air-gap deployments.

Updates in an air-gapped environment use a signed-bundle workflow: download and verify on a connected machine, transfer via approved removable media, then trigger local verification inside the air-gap.

Step 1 — Download the release bundle on a connected machine.

Obtain the versioned bundle and its detached signature from the Arbitex releases page:

outpost-X.Y.Z.tar.gz
outpost-X.Y.Z.tar.gz.sig

Step 2 — Verify the Ed25519 signature on the connected machine.

Before transferring, confirm the bundle is authentic:

Terminal window
# Decode the base64-encoded Ed25519 public key from your Outpost config
echo "$SOFTWARE_UPDATE_ED25519_KEY" | base64 -d > outpost-pubkey.bin
# Verify (requires openssl 3.x or a compatible Ed25519 tool)
openssl pkeyutl -verify \
-pubin -inkey outpost-pubkey.bin \
-sigfile outpost-X.Y.Z.tar.gz.sig \
-in <(sha256sum outpost-X.Y.Z.tar.gz | awk '{print $1}')

Alternatively, use the Outpost CLI verify command if available in your toolchain.

Step 3 — Transfer bundle and signature to the air-gapped environment via your approved removable media process.

Step 4 — Place both files in the stage directory.

/tmp/outpost-update-stage
# Override with SOFTWARE_UPDATE_STAGE_DIR
cp outpost-X.Y.Z.tar.gz /tmp/outpost-update-stage/
cp outpost-X.Y.Z.tar.gz.sig /tmp/outpost-update-stage/

Step 5 — Trigger local check via the admin API.

Terminal window
curl -X POST http://localhost:8080/admin/updates/check_local \
-H "Authorization: Bearer $OUTPOST_ADMIN_TOKEN"

Step 6 — Inspect the response.

A successful verification returns "status": "STAGED". The Outpost verifies the Ed25519 signature internally using cryptography.hazmat.primitives.asymmetric.ed25519 and extracts the bundle.

If signature verification fails:

{ "status": "VERIFY_FAILED", "reason": "signature mismatch" }

The bundle is not staged and the running Outpost is not affected. Re-download and re-transfer the bundle.

Step 7 — Restart the Outpost to apply the update.

Terminal window
kubectl rollout restart deployment/arbitex-outpost-airgap -n arbitex
Terminal window
SOFTWARE_UPDATE_RELEASE_URL="" # empty = online updates disabled
SOFTWARE_UPDATE_ED25519_KEY=<base64-key> # base64-encoded 32-byte Ed25519 public key
SOFTWARE_UPDATE_SIGNING_KEY=<base64-key> # alias for ED25519_KEY (either accepted)
SOFTWARE_UPDATE_STAGE_DIR=/tmp/outpost-update-stage

The Outpost exposes multiple independent monitoring integration points. All of them work in air-gap mode when their target service is inside the same isolated network.

The /metrics endpoint serves Prometheus text-format metrics. It requires no external dependencies and is always active regardless of air-gap state. A Prometheus server inside the air-gapped network can scrape it directly.

# prometheus.yml (inside the air-gapped network)
scrape_configs:
- job_name: arbitex-outpost
static_configs:
- targets: ['outpost:8080']
scrape_interval: 15s

Air-gap-relevant metrics to monitor:

Metric Description
outpost_airgap_bundle_age_seconds Age of the loaded policy bundle
outpost_dlp_scan_duration_seconds Per-tier DLP scan latency
outpost_policy_eval_total Policy evaluation counters
outpost_cert_expiry_seconds Seconds until TLS cert expiry
outpost_audit_queue_depth Events pending export from SQLite

When your air-gapped network includes a Prometheus Pushgateway, the Outpost can push metrics to it on a configurable interval:

Terminal window
PUSHGATEWAY_ENABLED=true
PUSHGATEWAY_URL=http://pushgateway:9091
PUSHGATEWAY_JOB=outpost # default
PUSHGATEWAY_INTERVAL_SECONDS=60 # default
PUSHGATEWAY_INSTANCE= # defaults to hostname

The Pushgateway exporter includes a circuit breaker: five consecutive push failures open the breaker for a 300-second recovery window before retrying.

When your air-gapped network includes an OTel Collector, the Outpost exports spans via OTLP gRPC:

Terminal window
OTEL_ENABLED=true
OTEL_EXPORTER_ENDPOINT=http://otel-collector:4317
OTEL_SERVICE_NAME=arbitex-outpost # default

When OTEL_ENABLED=false (the default), the Outpost uses a no-op tracer with zero overhead. Enabling tracing requires an OTel Collector reachable within the air-gapped network. There is no external OTel endpoint to configure for air-gap deployments.

The Outpost can write rotating JSONL or CSV audit log files to a local volume for ingestion by a log aggregator running inside the air-gapped network:

Terminal window
LOG_EXPORT_ENABLED=true
LOG_EXPORT_PATH=/var/log/outpost/audit/ # default
LOG_EXPORT_FORMAT=jsonl # or: csv
LOG_EXPORT_ROTATION_MB=100
LOG_EXPORT_MAX_FILES=10

Mount a PersistentVolume at LOG_EXPORT_PATH to retain logs across pod restarts. A local log aggregator (Fluentd, Filebeat, etc.) can tail the JSONL files directly.

The Outpost can write audit events directly to an internal syslog server or Splunk HEC endpoint:

Terminal window
SIEM_DIRECT_ENABLED=true
SIEM_DIRECT_TYPE=syslog # or: splunk_hec
SIEM_DIRECT_URL=udp://syslog:514

Both syslog and Splunk HEC targets operate entirely within the air-gapped network — no internet egress is required.

Feature Default Air-Gap Behavior Config key
Prometheus /metrics Always on No change None needed
Pushgateway push Off Works with internal Pushgateway PUSHGATEWAY_ENABLED=true
OTel traces Off Works with internal OTel Collector OTEL_ENABLED=true
Local log export Off Writes to local volume LOG_EXPORT_ENABLED=true
SIEM direct sink Off Works with internal SIEM SIEM_DIRECT_ENABLED=true
Heartbeat to Platform On (connected) Disabled — cannot enable N/A

Credential Intelligence (CredInt) uses a bloom filter to check bearer tokens and API keys against a breach corpus. In a connected deployment, the bloom filter is refreshed from the CredInt CDN periodically. In air-gap mode, you provision the bloom filter file manually.

Terminal window
CREDINT_ENABLED=true
CREDINT_BLOOM_PATH=/opt/arbitex/credint/corpus.bf # required in air-gap
CREDINT_CDN_URL="" # empty = no CDN refresh
CREDINT_REFRESH_INTERVAL_SECONDS=86400 # irrelevant when CDN URL is empty
  1. On a connected machine, download the latest bloom filter file from the Arbitex CredInt distribution endpoint.
  2. Verify the file integrity using the SHA256 checksum provided alongside the download.
  3. Transfer the .bf file to the air-gapped environment via approved removable media.
  4. Place the file at the path configured in CREDINT_BLOOM_PATH.
  5. Restart the Outpost to load the new bloom filter.

The bloom filter is not auto-updated in air-gap mode. Operators must repeat this procedure periodically to keep the breach corpus current. Arbitex releases updated bloom filter files on a regular schedule — check the release notes for the recommended refresh cadence.

Degraded behavior when bloom filter is unavailable

Section titled “Degraded behavior when bloom filter is unavailable”

If CREDINT_BLOOM_PATH is empty or the file is not found at startup, CredInt disables gracefully:

WARNING CredInt bloom filter not found at /opt/arbitex/credint/corpus.bf — Tier 4 scanning inactive

Tier 1, Tier 2, and Tier 3 DLP scanning remain fully active. Only Tier 4 credential breach detection is affected.


Symptom: WARNING Policy bundle not found at /opt/arbitex/policies/policy_bundle.json

Cause: The volume is not mounted, the mount path does not match AIRGAP_POLICY_PATH, or the file was not placed at the correct path inside the volume.

Resolution:

Terminal window
# Verify the env var
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
env | grep AIRGAP_POLICY_PATH
# Verify the file is present inside the pod
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
ls -la /opt/arbitex/policies/
# Verify the ConfigMap contains the bundle
kubectl get configmap arbitex-policy-bundle -n arbitex -o yaml | head -20

Symptom: WARNING DeBERTa model not found, Tier 3 disabled

Cause: model.onnx is not present at the resolved path, or onnxruntime is not installed in the container image.

Resolution:

Terminal window
# Check which path the Outpost is looking at
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
env | grep -E 'AIRGAP_MODEL_PATH|DEBERTA_MODEL_PATH'
# Verify the file exists on the mounted volume
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
ls -lh /opt/arbitex/models/
# Confirm onnxruntime is installed in the image
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
python -c "import onnxruntime; print(onnxruntime.__version__)"

Tier 1 and Tier 2 DLP remain active if Tier 3 fails to load. This is a graceful degradation, not a startup failure.


Symptom: WARNING GeoIP: no valid MMDB found, enrichment disabled

Cause: No MMDB file was found at any tier of the resolution chain (GEOIP_MMDB_PATH, download-on-start, GEOIP_MMDB_FALLBACK_PATH).

Resolution:

Terminal window
# Check configured paths
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
env | grep GEOIP
# Verify the MMDB file is accessible
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
ls -lh /opt/arbitex/geoip/

GeoIP enrichment is optional. The Outpost continues operating without it — audit records simply omit geographic fields.


Symptom: cert_expiry_days is negative or near zero in /admin/api/sync-status.

Cause: Manual cert rotation was not performed before expiry. In air-gap mode there is no automatic rotation scheduler.

Resolution:

Terminal window
# Check current cert expiry
curl http://localhost:8080/admin/api/sync-status \
-H "Authorization: Bearer $OUTPOST_ADMIN_TOKEN" | jq '.cert_expiry_days'
# After replacing cert/key files on the volume:
kubectl rollout restart deployment/arbitex-outpost-airgap -n arbitex

Set up a monitoring alert when cert_expiry_days drops below 45 days to ensure sufficient lead time for scheduling maintenance in air-gapped environments.


Symptom: audit_queue_depth reported by /admin/api/sync-status increases continuously and never drains.

Cause: In a permanent air-gap deployment there is no Platform connectivity to drain the queue to. This is expected behavior — the SQLite queue is the durable buffer.

Resolution options:

  1. Enable local log export to drain events to a file-based sink:
Terminal window
LOG_EXPORT_ENABLED=true
LOG_EXPORT_PATH=/var/log/outpost/audit/
LOG_EXPORT_FORMAT=jsonl
  1. Periodically extract events from the SQLite database directly:
Terminal window
kubectl exec -n arbitex deploy/arbitex-outpost-airgap -- \
sqlite3 /opt/arbitex/audit/audit_queue.db \
"SELECT event_json FROM local_audit_queue;" > audit-export.jsonl

Transfer the extracted file to the Platform via your approved offline transfer process.


Symptom: POST /admin/updates/check_local returns "status": "VERIFY_FAILED".

Cause: The signing key configured in SOFTWARE_UPDATE_ED25519_KEY does not match the key used to sign the bundle, or the bundle was corrupted during transfer.

Resolution:

Terminal window
# Verify the bundle checksum on the transfer medium before copying
sha256sum outpost-X.Y.Z.tar.gz
# Compare against the published SHA256 checksum from the Arbitex release page
# Re-download if they do not match
# Confirm the correct public key is configured
kubectl get secret arbitex-outpost-config -n arbitex -o jsonpath='{.data.SOFTWARE_UPDATE_ED25519_KEY}' \
| base64 -d

Re-download the bundle on the connected machine, verify the checksum before physical transfer, and retry check_local.


HMAC verification failure on policy bundle

Section titled “HMAC verification failure on policy bundle”

Symptom: ERROR Policy bundle HMAC verification failed

Cause: Either the POLICY_HMAC_KEY environment variable does not match the key used when the bundle was generated, or the bundle was generated more than BUNDLE_MAX_AGE_SECONDS (default: 3600 seconds) ago and has exceeded its validity window.

Resolution:

For air-gap deployments where the bundle is generated offline and age is not a meaningful constraint, disable HMAC verification:

Terminal window
POLICY_BUNDLE_VERIFY=false

Alternatively, regenerate the bundle with the correct key and ensure the bundle is transferred and loaded within the configured BUNDLE_MAX_AGE_SECONDS window. For development and test environments only:

Terminal window
INSECURE_SKIP_HMAC=true # dev/test only — do not use in production


The scripts/make-airgap.sh script runs on a machine with internet access and produces a self-contained tarball ready for transfer to the isolated host.

  • Docker Engine 24 or later with Compose V2 (docker compose version)
  • Access to the arbitex-outpost repository
  • (Optional) A MaxMind GeoLite2-City MMDB file at geoip/GeoLite2-City.mmdb — required for GeoIP-based routing and compliance features
  • Docker Engine 24 or later with Compose V2
  • A user account in the docker group, or root
  • systemd (optional — for automatic startup on boot)
  • Sufficient disk space: the package tarball is approximately 5–6 GB depending on configuration
  • Sufficient RAM: 2 GB minimum; 4 GB recommended for Tier 3 (DeBERTa) DLP

Before the Outpost can connect to the management plane, obtain three certificate files from Arbitex Cloud when you register the outpost:

File Purpose
outpost.pem Outpost mTLS client certificate
outpost.key Private key for the client certificate
ca.pem Arbitex CA certificate for server verification

Register the outpost in the Arbitex admin console and download the certificate bundle before starting the installation.

On the internet-connected build machine, from the arbitex-outpost project root:

Terminal window
bash scripts/make-airgap.sh

To build a specific version:

Terminal window
bash scripts/make-airgap.sh 1.2.0

If no version is provided, the script uses the current git tag if one exists on HEAD, or falls back to the current date (YYYYMMDD).

The script automatically:

  1. Verifies Docker Engine 24+ and Compose V2 are available
  2. Builds the CPU Docker image: arbitex/outpost:<VERSION>
  3. Builds the GPU Docker image: arbitex/outpost:<VERSION>-gpu (--build-arg INFERENCE_MODE=gpu)
  4. Saves both images to a single compressed tarball: dist/outpost-image-<VERSION>.tar.gz
  5. Stages compose file, .env.example, airgap-install.sh (renamed install.sh), Helm chart, default policy bundle bootstrap, and the GeoIP MMDB (if present)
  6. Generates image.sha256 checksum for the image tarball
  7. Archives everything into dist/arbitex-outpost-airgap-<VERSION>.tar.gz
  8. Writes a SHA-256 checksum file alongside the tarball

Output files:

File Description
dist/arbitex-outpost-airgap-<VERSION>.tar.gz Full air-gap package — transfer this to the target host
dist/arbitex-outpost-airgap-<VERSION>.tar.gz.sha256 SHA-256 checksum — transfer alongside the tarball
Terminal window
scp dist/arbitex-outpost-airgap-<VERSION>.tar.gz \
dist/arbitex-outpost-airgap-<VERSION>.tar.gz.sha256 \
user@target-host:/tmp/

On the target host, verify the package integrity before proceeding:

Terminal window
cd /tmp
sha256sum -c arbitex-outpost-airgap-<VERSION>.tar.gz.sha256

Expected output: arbitex-outpost-airgap-<VERSION>.tar.gz: OK. If verification fails, do not proceed.

Extract and run the installer:

Terminal window
tar -xzf arbitex-outpost-airgap-<VERSION>.tar.gz
cd airgap-<VERSION>/
bash install.sh

Checksum validation — the installer re-verifies the Docker image tarball checksum against the embedded image.sha256 file. If verification fails, installation stops.

Docker image load — both the CPU and GPU images are loaded into the local Docker daemon from the tarball (docker load). No network access is required. This step may take 2–5 minutes.

Configuration prompts — the installer prompts for the following values. Pre-set them as environment variables for unattended installation (see below).

Prompt Environment variable Required Notes
Outpost ID OUTPOST_ID Yes UUID issued at outpost registration (Arbitex admin console)
Platform URL PLATFORM_MANAGEMENT_URL No Defaults to https://api.arbitex.ai
Audit HMAC key AUDIT_HMAC_KEY Yes Long random string; generate with openssl rand -hex 32
Emergency admin key OUTPOST_EMERGENCY_ADMIN_KEY No Local admin access when management plane is unreachable
GPU mode GPU_MODE No Enter y to use the GPU image; defaults to CPU

The audit HMAC key seeds the tamper-evident chain on the local audit log. Retain it — audit log verification requires the original key.

Install directory — default is /opt/arbitex-outpost. Override with INSTALL_DIR.

Policy cache bootstrap — the installer places the included default-policy-bundle.json as a bootstrap bundle so the Outpost can start without network access. Until the Outpost reaches the management plane, no DLP rules or provider routing are active. Add mTLS certificates before allowing user traffic.

GeoIP database — if GeoLite2-City.mmdb was included in the package, the installer copies it to ${INSTALL_DIR}/geoip/ and sets MAXMIND_DB_PATH=/app/geoip/GeoLite2-City.mmdb in .env. If absent, a warning is printed and GeoIP features are disabled.

Runtime directory creation — the installer creates three required directories:

Directory Purpose
certs/ mTLS certificate files
audit_buffer/ Local HMAC-chained audit log
policy_cache/ Cached policy bundle

systemd service — if systemd is running, the installer offers to install an arbitex-outpost service for automatic startup on boot (requires root). To skip, answer N; the installer starts the Outpost directly with docker compose up -d.

Property Value
Service name arbitex-outpost
Service file /etc/systemd/system/arbitex-outpost.service
Start command docker compose -f docker-compose.outpost.yml up
Restart policy Always, with 10-second delay

After installation, place the mTLS certificate files and restart:

Terminal window
cp outpost.pem outpost.key ca.pem /opt/arbitex-outpost/certs/
sudo systemctl restart arbitex-outpost

The package includes both a CPU image and a GPU image. Mode is selected at installation. To change mode after installation, edit .env and the image tag in docker-compose.outpost.yml, then restart.

CPU mode (default): DeBERTa Tier 3 DLP runs on CPU — enable with DLP_DEBERTA_ENABLED=true. CPU DeBERTa inference adds 50–500 ms per request depending on text length.

GPU mode: Requires NVIDIA GPU with CUDA support and the NVIDIA Container Toolkit (nvidia-docker2). Reduces DeBERTa inference to 10–50 ms and enables higher request throughput.

Pre-set environment variables to skip all interactive prompts:

Terminal window
export OUTPOST_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
export PLATFORM_MANAGEMENT_URL="https://api.arbitex.ai"
export AUDIT_HMAC_KEY="$(openssl rand -hex 32)"
export OUTPOST_EMERGENCY_ADMIN_KEY="$(openssl rand -hex 24)"
export GPU_MODE="N"
export INSTALL_DIR="/opt/arbitex-outpost"
bash install.sh

systemd (recommended)

Terminal window
sudo systemctl status arbitex-outpost
sudo systemctl start arbitex-outpost
sudo systemctl stop arbitex-outpost
sudo systemctl restart arbitex-outpost # required after .env changes or cert replacement
# Logs
journalctl -u arbitex-outpost -f
journalctl -u arbitex-outpost -n 200
journalctl -u arbitex-outpost --since "2026-03-01 00:00:00"

Docker Compose (without systemd)

Terminal window
cd /opt/arbitex-outpost
docker compose -f docker-compose.outpost.yml ps
docker compose -f docker-compose.outpost.yml up -d
docker compose -f docker-compose.outpost.yml down
docker compose -f docker-compose.outpost.yml logs -f
docker compose -f docker-compose.outpost.yml restart
Terminal window
curl http://localhost:8300/healthz # liveness — 200 if process is running
curl http://localhost:8300/readyz # readiness — 200 if policy bundle is loaded; 503 if not

Application logs (systemd journal) — rotated automatically by journald. To cap journal storage for the Outpost service, set SystemMaxUse=500M in /etc/systemd/journald.conf and restart journald.

Audit buffer — the audit buffer at audit_buffer/audit.jsonl is managed as a ring buffer by the Outpost. The maximum number of entries is set by MAX_AUDIT_BUFFER_ENTRIES (default: 100,000). When the buffer is full, the oldest entries are rotated out automatically.

Monitor buffer size periodically in long-running air-gap deployments:

Terminal window
wc -l /opt/arbitex-outpost/audit_buffer/audit.jsonl
du -sh /opt/arbitex-outpost/audit_buffer/audit.jsonl

SIEM dead-letter file — if SIEM_DIRECT_DEAD_LETTER_PATH is set, failed SIEM deliveries are appended without rotation. Add an OS-level logrotate entry:

/etc/logrotate.d/arbitex-outpost-siem
/var/log/arbitex/siem-dead-letter.jsonl {
daily
rotate 30
compress
missingok
notifempty
copytruncate
}

Audit log extraction for manual SIEM import

Section titled “Audit log extraction for manual SIEM import”

In air-gapped environments without a SIEM direct sink, audit events accumulate in the local buffer. Extract them manually for import into your SIEM system:

Terminal window
cp /opt/arbitex-outpost/audit_buffer/audit.jsonl \
/tmp/arbitex-audit-$(date +%Y%m%d-%H%M%S).jsonl

The file is JSONL format (one JSON object per line). Each entry includes:

Field Description
event_id Unique event UUID
outpost_id Outpost identifier
timestamp ISO 8601 event time
request_id Request correlation ID
user_id User who made the request
model Target model
dlp_action DLP outcome: ALLOW, BLOCK, REDACT, PROMPT
dlp_entities List of detected entity types
hmac HMAC-SHA256 chain signature
prev_hmac Previous entry’s HMAC (chain link)

Verify HMAC chain integrity before importing — see Audit log verification for the full procedure.

Splunk import:

Terminal window
/opt/splunkforwarder/bin/splunk add oneshot /tmp/arbitex-audit-<DATE>.jsonl \
-sourcetype arbitex:outpost:audit \
-index arbitex_audit

For Microsoft Sentinel, use the custom log ingestion API or upload the JSONL file via the Log Analytics workspace data upload feature.

When the Outpost can temporarily reach the management plane, the PolicySyncClient automatically fetches the latest bundle. To push a specific bundle to an Outpost with no management plane connectivity:

  1. Export the policy bundle from the cloud portal as a JSON file.
  2. Transfer it to the air-gapped host.
  3. Replace the cached bundle:
Terminal window
cp exported-policy-bundle.json /opt/arbitex-outpost/policy_cache/policy_bundle.json
  1. Restart the Outpost to reload the bundle:
Terminal window
sudo systemctl restart arbitex-outpost