Runbook: Concelier Air-Gap Bundle Deploy (CONCELIER-AIRGAP-56-003)

Status: draft · 2025-11-24 · editorially reconciled 2026-05-31

Purpose: Deploy a sealed-mode Concelier evidence bundle — deterministic NDJSON plus a manifest and entry-trace — into an offline (air-gapped) Concelier node, and verify the import end to end. Audience: Platform / DevOps operators staging vulnerability feeds into an air-gapped Stella Ops deployment where no external egress is permitted.

Metadata

FieldValue
ComponentConcelier (air-gap bundle ingestion)
SeverityMedium (offline feed currency)
On-call scopePlatform team
Last updated2026-05-31
Feature gateAir-gap mode must be enabled on the Concelier WebService

Endpoint reconciliation (2026-05-31 — flag for follow-up). This draft predates the current Concelier air-gap HTTP surface and references endpoints that were not found in source: POST /internal/airgap/import and GET /internal/airgap/status on an anonymous http://localhost:5000. The wired, tenant-scoped surface today is the /api/v1/concelier/... group (verified in src/Concelier/StellaOps.Concelier.WebService/Extensions/):

  • Import: POST /api/v1/concelier/imports/ (validate first with POST /api/v1/concelier/imports/validate; poll progress with GET /api/v1/concelier/imports/{importId}) — all require the Concelier.Advisories.Ingest / Concelier.Advisories.Read policies and a tenant context, not an unauthenticated localhost call.
  • Status / catalog: GET /api/v1/concelier/airgap/status, GET /api/v1/concelier/airgap/catalog, and the POST /api/v1/concelier/airgap/bundles/{bundleId}/import flow.

The original NDJSON-curl steps below are preserved as-authored, but treat the route, port, and authentication details as unverified until this runbook is reconciled against the live API. Likewise, the concelier:features:airgap feature key and the STELLAOPS_AIRGAP_IMPORT_DIR / CONCELIER_IMPORT__STAGINGROOT environment variables below could not be confirmed in source (the WebService binds air-gap mode from Concelier:AirGap:Enabled); verify against your deployment before relying on them.


Inputs

ArtifactFileNotes
Bundleconcelier-airgap.ndjsonDeterministic NDJSON, lexicographically ordered
Manifestbundle.manifest.jsonCarries bundleSha256 and bundle metadata
Entry tracebundle.entry-trace.jsonPer-entry SHA-256 trace

All hashes (SHA-256) are recorded in the manifest and entry-trace — verify them before import.

Preconditions

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/. The volume is mounted read-only — Concelier reads and validates bundles but never modifies the staging directory.

# 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 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:

SourceDescriptionVolume needed?
Server PathPath inside the container (e.g. /var/lib/concelier/import/bundle.tar.gz). Zero browser transfer.Yes
URLInternal URL the backend downloads directly.No
File UploadBrowser 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 it to the offline controller host).

  2. Verify hashes before importing:

    # Bundle hash must match the manifest's recorded bundleSha256
    sha256sum concelier-airgap.ndjson | diff - <(jq -r .bundleSha256 bundle.manifest.json)
    
    # Cross-check the per-entry hashes from the entry-trace
    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 the bundle:

    curl -sSf -X POST \
      -H 'Content-Type: application/x-ndjson' \
      --data-binary @concelier-airgap.ndjson \
      http://localhost:5000/internal/airgap/import
    
  4. Validate the import:

    curl -sSf http://localhost:5000/internal/airgap/status | jq
    
  5. Record evidence: store the manifest and entry-trace alongside the TRX/logs in artifacts/airgap/<date>/.

Determinism notes

Rollback

Delete the imported batch by bundleId from the advisory_observations and advisory_linksets tables (requires DBA approval), then re-run the import after fixing the hash mismatch.