# Concelier Air-Gap Bundle Deploy Runbook (CONCELIER-AIRGAP-56-003) Status: draft · 2025-11-24 Scope: deploy sealed-mode Concelier evidence bundles using deterministic NDJSON + manifest/entry-trace outputs. ## Inputs - Bundle: `concelier-airgap.ndjson` - Manifest: `bundle.manifest.json` - Entry trace: `bundle.entry-trace.json` - Hashes: SHA256 recorded in manifest and entry-trace; verify before import. ## Preconditions - Concelier WebService running with `concelier:features:airgap` enabled. - No external egress; only local file system allowed for bundle path. - PostgreSQL indexes applied (`advisory_observations`, `advisory_linksets` tables). - **Import volume mounted**: The Concelier container must have the import staging directory mounted. In Docker Compose this is configured via `STELLAOPS_AIRGAP_IMPORT_DIR` (defaults to `./airgap-import` on the host, mounted read-only at `/var/lib/concelier/import` inside the container). ## Import Volume Setup (Docker Compose) The Concelier service mounts an import staging volume for air-gapped bundle ingestion. Bundles placed on the host at `$STELLAOPS_AIRGAP_IMPORT_DIR` are visible inside the container at `/var/lib/concelier/import/`. ```bash # Default: ./airgap-import relative to the compose directory mkdir -p devops/compose/airgap-import # Override: point to USB, NFS mount, or any host directory export STELLAOPS_AIRGAP_IMPORT_DIR=/media/usb/stellaops-bundles docker compose -f docker-compose.stella-ops.yml up -d concelier ``` The volume is mounted **read-only** — the Concelier service reads and validates bundles but never modifies the staging directory. The environment variable `CONCELIER_IMPORT__STAGINGROOT` tells the service where to find staged bundles inside the container. ### UI Console Import The Feeds & Airgap console (Ops → Operations → Feeds & Airgap → Airgap Bundles → Import) supports three import sources: | Source | Description | Volume needed? | |---|---|---| | **Server Path** | Path inside the container (e.g. `/var/lib/concelier/import/bundle.tar.gz`). Zero browser transfer. | Yes | | **URL** | Internal URL the backend downloads directly. | No | | **File Upload** | Browser drag-and-drop for small bundles. | No | For large bundles (GB+), use **Server Path** or **URL** — never browser upload. ## Steps 1) Stage the bundle onto the import volume (or transfer to the offline controller host). 2) Verify hashes: ```bash sha256sum concelier-airgap.ndjson | diff - <(jq -r .bundleSha256 bundle.manifest.json) jq -r '.[].sha256' bundle.entry-trace.json | nl | sed 's/\t/:/' > entry.hashes paste -d' ' <(cut -d: -f1 entry.hashes) <(cut -d: -f2 entry.hashes) ``` 3) Import: ```bash curl -sSf -X POST \ -H 'Content-Type: application/x-ndjson' \ --data-binary @concelier-airgap.ndjson \ http://localhost:5000/internal/airgap/import ``` 4) Validate import: ```bash curl -sSf http://localhost:5000/internal/airgap/status | jq ``` 5) Record evidence: - Store manifest + entry-trace alongside TRX/logs in `artifacts/airgap//`. ## Determinism notes - NDJSON ordering is lexicographic; do not re-sort downstream. - Entry-trace hashes must match post-transfer; any mismatch aborts import. ## Rollback - Delete imported batch by `bundleId` from `advisory_observations` and `advisory_linksets` (requires DBA approval); rerun import after fixing hash.