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
| Field | Value |
|---|---|
| Component | Concelier (air-gap bundle ingestion) |
| Severity | Medium (offline feed currency) |
| On-call scope | Platform team |
| Last updated | 2026-05-31 |
| Feature gate | Air-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/importandGET /internal/airgap/statuson an anonymoushttp://localhost:5000. The wired, tenant-scoped surface today is the/api/v1/concelier/...group (verified insrc/Concelier/StellaOps.Concelier.WebService/Extensions/):
- Import:
POST /api/v1/concelier/imports/(validate first withPOST /api/v1/concelier/imports/validate; poll progress withGET /api/v1/concelier/imports/{importId}) — all require theConcelier.Advisories.Ingest/Concelier.Advisories.Readpolicies and a tenant context, not an unauthenticated localhost call.- Status / catalog:
GET /api/v1/concelier/airgap/status,GET /api/v1/concelier/airgap/catalog, and thePOST /api/v1/concelier/airgap/bundles/{bundleId}/importflow.The original NDJSON-
curlsteps 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, theconcelier:features:airgapfeature key and theSTELLAOPS_AIRGAP_IMPORT_DIR/CONCELIER_IMPORT__STAGINGROOTenvironment variables below could not be confirmed in source (the WebService binds air-gap mode fromConcelier:AirGap:Enabled); verify against your deployment before relying on them.
Inputs
| Artifact | File | Notes |
|---|---|---|
| Bundle | concelier-airgap.ndjson | Deterministic NDJSON, lexicographically ordered |
| Manifest | bundle.manifest.json | Carries bundleSha256 and bundle metadata |
| Entry trace | bundle.entry-trace.json | Per-entry SHA-256 trace |
All hashes (SHA-256) are recorded in the manifest and entry-trace — verify them before import.
Preconditions
- Concelier WebService is running with air-gap mode enabled (feature gate — see the reconciliation note above).
- No external egress; only the local file system is permitted for the bundle path.
- PostgreSQL indexes are applied for the
advisory_observationsandadvisory_linksetstables. - The import staging directory is mounted into the Concelier container (see below).
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:
| 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
Stage the bundle onto the import volume (or transfer it to the offline controller host).
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)Import the bundle:
curl -sSf -X POST \ -H 'Content-Type: application/x-ndjson' \ --data-binary @concelier-airgap.ndjson \ http://localhost:5000/internal/airgap/importValidate the import:
curl -sSf http://localhost:5000/internal/airgap/status | jqRecord evidence: store the manifest and entry-trace alongside the TRX/logs in
artifacts/airgap/<date>/.
Determinism notes
- NDJSON ordering is lexicographic; do not re-sort downstream.
- Entry-trace hashes must match post-transfer; any mismatch aborts the import.
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.
Related Resources
- Air-gap operations (umbrella):
../airgap-operations-runbook.md - Concelier mirror operations:
../../modules/concelier/operations/mirror.md - Concelier mirror export:
../../modules/concelier/mirror-export.md - Related connector runbooks:
connector-nvd.md,connector-ghsa.md,connector-osv.md - Source of truth (air-gap endpoints):
src/Concelier/StellaOps.Concelier.WebService/Extensions/FeedMirrorManagementEndpoints.cs,.../Extensions/AirGapEndpointExtensions.cs,.../Options/AirGapOptions.cs
