Offline Kit (OUK) — Air-Gap Bundle

Audience: operators deploying or updating Stella Ops on air-gapped or otherwise network-restricted hosts.

The Offline Kit (historically the Offline Update Kit, “OUK”) packages everything Stella Ops needs to run on a completely isolated network:

ComponentContents
Merged vulnerability feedsOSV, GHSA plus optional NVD 2.0, CNNVD, CNVD, ENISA, JVN and BDU
Container imagesstella-ops, Zastava sidecar, advisory-ai-web, and advisory-ai-worker (x86-64 & arm64)
ProvenanceCosign signature, SPDX 2.3 SBOM, in-toto SLSA attestation
Attested manifestoffline-manifest.json + detached JWS covering bundle metadata, signed during export.
Signed release manifestmanifest/product-update-manifest.dsse.json plus release trust roots under manifest/trust-roots/ for offline update-channel verification.
Delta patchesDaily diff bundles keep size < 350 MB
Scanner plug-insOS analyzers plus the Node.js, Go, .NET, Python, Ruby, Rust, and PHP language analyzers packaged under plugins/scanner/analyzers/** with manifests so Workers load deterministically offline.
Debug store.debug artefacts laid out under debug/.build-id/<aa>/<rest>.debug with debug/debug-manifest.json mapping build-ids to originating images for symbol retrieval.
Secret Detection RulesDSSE-signed rule bundles under rules/secrets/<version>/ with manifest, JSONL rules, and signature envelope for air-gapped secret leak detection.
Telemetry collector bundletelemetry/telemetry-offline-bundle.tar.gz plus .sha256, containing OTLP collector config, Helm/Compose overlays, and operator instructions.
CLI + Task Packscli/ binaries from release/cli, Task Runner bootstrap (bootstrap/task-runner/task-runner.yaml.sample), and task-pack docs under docs/modules/packs-registry/guides/** + docs/modules/taskrunner/**.
Orchestrator/Export/Notifier kitsOrchestrator service, worker SDK, Postgres snapshot, dashboards (jobengine/**), Export Center bundles (export-center/**), Notifier offline packs (notifier/**).
Container air-gap bundlesAny tar/tgz under containers/ or images/ (mirrored registries) plus docs/modules/airgap/guides/mirror-bundles.md.
Surface.SecretsEncrypted secrets bundles and manifests (surface-secrets/**) for sealed-mode bootstrap.

RU BDU note: ship the official Russian Trusted Root/Sub CA bundle (certificates/russian_trusted_bundle.pem) inside the kit so concelier:httpClients:source.bdu:trustedRootPaths can resolve it when the service runs in an air-gapped network. Drop the most recent vulxml.zip alongside the kit if operators need a cold-start cache.

Language analyzers: the kit now carries the restart-only Node.js, Go, .NET, Python, Ruby, Rust, and PHP plug-ins (plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Node/, ...Lang.Go/, ...Lang.DotNet/, ...Lang.Python/, ...Lang.Ruby/, ...Lang.Rust/, ...Lang.Php/). Drop the directories alongside Worker binaries so the unified plug-in catalog can load them without outbound fetches.

Ruby analyzer features:

The PHP analyzer parses composer.lock for Composer dependencies and supports optional runtime evidence via the stella-trace.php shim; set STELLA_PHP_OPCACHE=1 to enable opcache statistics collection.

Secret Detection Rules:

The Offline Kit includes DSSE-signed rule bundles for secret leak detection, enabling fully offline scanning for exposed credentials, API keys, and other sensitive data.

Bundle Structure:

rules/secrets/<version>/
  secrets.ruleset.manifest.json    # Bundle metadata (version, rule count, signer)
  secrets.ruleset.rules.jsonl      # Rule definitions (one JSON per line)
  secrets.ruleset.dsse.json        # DSSE signature envelope
  SHA256SUMS                       # File checksums

Manifest Format:

{
  "bundleId": "secrets.ruleset",
  "bundleType": "secrets",
  "version": "2026.01",
  "ruleCount": 150,
  "signerKeyId": "stellaops-secrets-signer",
  "signedAt": "2026-01-04T00:00:00Z",
  "files": [
    {
      "name": "secrets.ruleset.rules.jsonl",
      "digest": "sha256:...",
      "sizeBytes": 45678
    }
  ]
}

Installation:

# Verify bundle signature using local attestor mirror
export STELLA_ATTESTOR_URL="file:///mnt/offline-kit/attestor-mirror"
devops/offline/scripts/install-secrets-bundle.sh \
  /mnt/offline-kit/rules/secrets/2026.01 \
  /opt/stellaops/plugins/scanner/analyzers/secrets

Bundle Rotation:

# Upgrade to new version with automatic backup
devops/offline/scripts/rotate-secrets-bundle.sh \
  /mnt/offline-kit/rules/secrets/2026.02

Enable Feature:

scanner:
  features:
    experimental:
      secret-leak-detection: true

Verify Bundle is Loaded:

kubectl logs -l app=scanner-worker --tail=100 | grep SecretsAnalyzerHost
# Expected: SecretsAnalyzerHost: Loaded bundle 2026.01 with 150 rules

Python analyzer features:

Surface.Env configuration: Scanner Worker and WebService components use the Surface.Env library for configuration discovery. In air-gapped deployments, configure the following environment variables (see docs/modules/scanner/design/surface-env.md for details):

VariableDescriptionAir-gap Default
SCANNER_SURFACE_FS_ENDPOINTBase URI for Surface.FS / RustFS storagehttp://rustfs:8080
SCANNER_SURFACE_FS_BUCKETBucket for manifests/artefactssurface-cache
SCANNER_SURFACE_CACHE_ROOTLocal cache directory/var/lib/stellaops/surface
SCANNER_SURFACE_CACHE_QUOTA_MBCache quota in MB (64-262144)4096
SCANNER_SURFACE_PREFETCH_ENABLEDEnable manifest prefetchfalse
SCANNER_SURFACE_TENANTTenant namespacedefault
SCANNER_SURFACE_SECRETS_PROVIDERSecrets provider (file, kubernetes)file
SCANNER_SURFACE_SECRETS_ROOTRoot path for file provider/etc/stellaops/secrets
SCANNER_SURFACE_SECRETS_ALLOW_INLINEAllow inline secretsfalse

For Helm deployments, configure via values.yaml:

surface:
  fs:
    endpoint: "http://rustfs:8080"
    bucket: "surface-cache"
  cache:
    root: "/var/lib/stellaops/surface"
    quotaMb: 4096
  tenant: "default"
  secrets:
    provider: "file"
    root: "/etc/stellaops/secrets"

For Docker Compose, these variables are pre-configured in docker-compose.airgap.yaml with sensible defaults.

Advisory AI volume primer: ship a tarball containing empty queue/, plans/, and outputs/ directories plus their ownership metadata. During import, extract it onto the RWX volume used by advisory-ai-web and advisory-ai-worker so pods start with the expected directory tree even on air-gapped nodes.

Scanner core: C# 12 on .NET 10 LTS. Imports are idempotent and atomic — no service downtime.

0 · Prepare the debug store

Before packaging the Offline Kit, mirror the release debug artefacts (GNU build-id .debug files and the associated manifest) into the staging directory:

./ops/offline-kit/mirror_debug_store.py \
  --release-dir out/release \
  --offline-kit-dir out/offline-kit

The helper copies debug/.build-id/**, validates debug/debug-manifest.json against its recorded SHA-256, and writes out/offline-kit/metadata/debug-store.json with a short summary (platforms, artefact counts, sample build-ids). The command exits non-zero if an artefact referenced by the manifest is missing or has the wrong digest, so run it as part of every kit build.


0.1 · Automated packaging

The packaging workflow is scripted via ops/offline-kit/build_offline_kit.py.
It verifies the release artefacts, runs the Python analyzer smoke suite, mirrors the debug store, and emits a deterministic tarball + manifest set.

What it picks up automatically (if present under --release-dir):

python ops/offline-kit/build_offline_kit.py \
  --version 2025.10.0 \
  --channel edge \
  --release-dir out/release \
  --staging-dir out/offline-kit/staging \
  --output-dir out/offline-kit/dist

# Optional: regenerate the telemetry collector bundle prior to packaging.
python ops/devops/telemetry/package_offline_bundle.py --output out/telemetry/telemetry-offline-bundle.tar.gz

Outputs:

Policy Engine configuration bundle (formerly Policy Gateway)

Note: The Policy Gateway service has been merged into Policy Engine. All gateway endpoints (exceptions, deltas, gates, governance, tool-lattice) are now served directly by the policy-engine process.

Provide --cosign-key / --cosign-identity-token (and optional --cosign-password) to generate Cosign signatures for both the tarball and manifest.


1 · Download & verify

curl -LO https://get.stella-ops.org/ouk/stella-ops-offline-kit-<DATE>.tgz
curl -LO https://get.stella-ops.org/ouk/stella-ops-offline-kit-<DATE>.tgz.sig
curl -LO https://get.stella-ops.org/ouk/offline-manifest-<DATE>.json
curl -LO https://get.stella-ops.org/ouk/offline-manifest-<DATE>.json.jws

cosign verify-blob \
  --key https://stella-ops.org/keys/cosign.pub \
  --signature stella-ops-offline-kit-<DATE>.tgz.sig \
  stella-ops-offline-kit-<DATE>.tgz

CLI shortcut. stellaops-cli offline kit pull --destination ./offline-kit downloads the bundle, manifest, and detached signatures in one step, resumes partial transfers, and writes a .metadata.json summary for later import.

Verification prints OK and the SHA-256 digest; cross-check against the changelog.

Validate the attested manifest before distribution:

cosign verify-blob \
  --key https://stella-ops.org/keys/cosign.pub \
  --signature offline-manifest-<DATE>.json.jws \
  offline-manifest-<DATE>.json

jq '.artifacts[] | {name, sha256, size, capturedAt}' offline-manifest-<DATE>.json

The manifest enumerates every artefact (name, sha256, size, capturedAt) and is signed with the same key registry as Authority revocation bundles. Operators can ship the manifest alongside the tarball so downstream mirrors can re-verify without unpacking the kit.

The product update manifest is a separate Signer DSSE envelope that binds the release version, image digests, SBOM references, support policy, and fixed advisory list. Before importing an OUK, verify it without network access:

stella verify release-manifest \
  ./manifest/product-update-manifest.dsse.json \
  --trust-root ./manifest/trust-roots \
  --json

build_offline_kit.py copies Signer-produced manifests named product-update-manifest.dsse.json, product-update-manifest.v1.dsse.json, or release-manifest.dsse.json from the release output into manifest/. It also copies release trust roots named product-update-trust-root.pem, release-trust-root.pem, or trust-root.pem into manifest/trust-roots/. The bundle metadata exposes counts.releaseManifestSignatures and counts.releaseTrustRoots so operators can reject incomplete release payloads before unpacking.

Example excerpt (2025-10-23 kit) showing the Go and .NET analyzer plug-in payloads:

{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.dll",
  "sha256": "a6dc850fc51151c8967ef46a3c4730f08b549667e041079431f39a8a72d0b641",
  "size": 33792,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.pdb",
  "sha256": "6cbdabf155282f458b89edf267e7f6bb2441a93029aad7aad45c8a9ec58b1b3b",
  "size": 32152,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/manifest.json",
  "sha256": "c19bfca2fcbb7cb18f1082b5d0d5a8f15fc799c648b50e95fce8d8b109ce48c9",
  "size": 622,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.dll",
  "sha256": "0734d23e33277ce2ccb596782d2d42cfe394b3d372dc34da9cb28b59df9b9d22",
  "size": 70144,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.pdb",
  "sha256": "b853c1ff4b196715f5bd1447e1a13edeb4940917527ec9bf153b5048da49abaf",
  "size": 40400,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/manifest.json",
  "sha256": "5d483885f825f01bfd9943dcf2889ec2e0beba38ede92ecfe67d4f506cf14e37",
  "size": 647,
  "capturedAt": "2025-10-23T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.dll",
  "sha256": "a4f558f363394096e3dd6263f35b180b93b4112f9cf616c05872da8a8657d518",
  "size": 47104,
  "capturedAt": "2025-10-26T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.pdb",
  "sha256": "ef2ad78bc2cd1d7e99bae000b92357aa9a9c32938501899e9033d001096196d0",
  "size": 31896,
  "capturedAt": "2025-10-26T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/manifest.json",
  "sha256": "668ad9a1a35485628677b639db4d996d1e25f62021680a81a22482483800e557",
  "size": 648,
  "capturedAt": "2025-10-26T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.dll",
  "sha256": "<computed-at-release>",
  "size": 0,
  "capturedAt": "2025-11-27T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.pdb",
  "sha256": "<computed-at-release>",
  "size": 0,
  "capturedAt": "2025-11-27T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Ruby/manifest.json",
  "sha256": "<computed-at-release>",
  "size": 0,
  "capturedAt": "2025-11-27T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Rust/StellaOps.Scanner.Analyzers.Lang.Rust.dll",
  "sha256": "d90ba8b6ace7d98db563b1dec178d57ac09df474e1342fa1daa38bd55e17b185",
  "size": 54784,
  "capturedAt": "2025-11-01T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Rust/StellaOps.Scanner.Analyzers.Lang.Rust.pdb",
  "sha256": "6fac88640a4980d2bb8f7ea2dd2f3d0a521b90fd30ae3a84981575d5f76fa3df",
  "size": 36636,
  "capturedAt": "2025-11-01T00:00:00Z"
}
{
  "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Rust/manifest.json",
  "sha256": "1ec47d1a2103ad5eff23e903532cb76b1ed7ded85d301c1a6631ff21aa966ed4",
  "size": 658,
  "capturedAt": "2025-11-01T00:00:00Z"
}

2 · Import on the air-gapped host

docker compose --env-file .env \
  -f docker-compose.stella-ops.yml \
  exec stella-ops \
  stella admin import-offline-usage-kit stella-ops-offline-kit-<DATE>.tgz

Alternatively, run

stellaops-cli offline kit import stella-ops-offline-kit-<DATE>.tgz \
  --manifest offline-manifest-<DATE>.json \
  --bundle-signature stella-ops-offline-kit-<DATE>.tgz.sig \
  --manifest-signature offline-manifest-<DATE>.json.jws

The CLI validates recorded digests (when .metadata.json is present) before streaming the multipart payload to /api/offline-kit/import.

2.1 Validator + idempotency enablement (air-gap)

The Offline Kit carries the same helper scripts under scripts/:

  1. Duplicate audit: run
    psql -d concelier -f ops/devops/scripts/check-advisory-raw-duplicates.sql -v LIMIT=200
    
    to verify no (vendor, upstream_id, content_hash, tenant) conflicts remain before enabling the idempotency index.
  2. Apply validators: execute psql -d concelier -f ops/devops/scripts/apply-aoc-validators.sql (and the Excititor equivalent) with validationLevel: "moderate" in maintenance mode.
  3. Restart Concelier so migrations 20251028_advisory_raw_idempotency_index and 20251028_advisory_supersedes_backfill run automatically. After the restart:
    • Confirm db.advisory resolves to a view on advisory_backup_20251028.
    • Spot-check a few advisory_raw entries to ensure supersedes chains are populated deterministically.
  4. Smoke test: run stella sources ingest --dry-run --fixture advisory (bundled fixtures) to confirm ingestion succeeds post-guard and the CLI reports zero violations.

Authority scope sanity check

Offline installs rely on the bundled etc/authority.yaml.sample. Before promoting the kit, confirm the sample clients keep the Aggregation-Only guardrails:

Authority now rejects tokens that request advisory:read, vex:read, or any signals:* scope without aoc:verify; the sample has been updated to match. Air-gap scopes (airgap:*) also require an explicit tenant assignment—match the updated roles (airgap-viewer, airgap-operator, airgap-admin) so automation fails closed when misconfigured.

Quick smoke test: before import, verify the tarball carries the Go analyzer plug-in:

tar -tzf stella-ops-offline-kit-<DATE>.tgz 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Ruby/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Php/*'

The manifest lookup above and this tar listing should both surface the Go analyzer DLL, PDB, and manifest entries before the kit is promoted.

Release guardrail. The automated release pipeline now publishes the Python, Ruby, Rust, and PHP plug-ins from source and executes dotnet run --project src/Tools/LanguageAnalyzerSmoke --configuration Release -- --repo-root <checkout> --analyzer <id> to validate manifest integrity and cold/warm determinism within the < 30 s / < 5 s budgets (differences versus repository goldens are logged for triage). Run ops/offline-kit/run-python-analyzer-smoke.sh, ops/offline-kit/run-ruby-analyzer-smoke.sh, ops/offline-kit/run-rust-analyzer-smoke.sh, and ops/offline-kit/run-php-analyzer-smoke.sh locally before shipping a refreshed kit if you rebuild artefacts outside CI or when preparing the air-gap bundle.

Debug store mirror

Offline symbols (debug/.build-id/**) must accompany every Offline Kit to keep symbol lookup deterministic. The release workflow is expected to emit out/release/debug/ containing the build-id tree plus debug-manifest.json and its .sha256 companion. After a release completes:

python ops/offline-kit/mirror_debug_store.py \
  --release-dir out/release \
  --offline-dir out/offline-kit \
  --summary out/offline-kit/metadata/debug-store.json

The script mirrors the debug tree into the Offline Kit staging directory, verifies SHA-256 values against the manifest, and writes a summary under metadata/debug-store.json for audit logs. If the release pipeline does not populate out/release/debug, the tooling now logs a warning (DEVOPS-REL-17-004)—treat it as a build failure and re-run the release once symbol extraction is enabled.


2.2 · Reachability & Proof Bundle Extensions

The Offline Kit supports deterministic replay and reachability analysis in air-gapped environments through additional bundle types.

Reachability Bundle Format

/offline/reachability/<scan-id>/
  ├── callgraph.json.zst         # Compressed call-graph (cg_node + cg_edge)
  ├── manifest.json               # Scan manifest with frozen feed hashes
  ├── manifest.dsse.json          # DSSE signature envelope
  ├── entrypoints.json            # Discovered entry points
  └── proofs/
      ├── score_proof.cbor        # Canonical CBOR proof ledger
      ├── score_proof.dsse.json   # DSSE signature for proof
      └── reachability.json       # Reachability verdicts per finding

Bundle contents:

FilePurposeFormat
callgraph.json.zstStatic call-graph extracted from artifactZstd-compressed JSON
manifest.jsonScan parameters + frozen Concelier/Excititor snapshot hashesJSON
manifest.dsse.jsonDSSE envelope signing the manifestJSON (in-toto DSSE)
entrypoints.jsonDiscovered entry points (controllers, handlers, etc.)JSON array
proofs/score_proof.cborDeterministic proof ledger with Merkle rootCBOR (RFC 8949)
proofs/score_proof.dsse.jsonDSSE signature attesting to proof integrityJSON (in-toto DSSE)
proofs/reachability.jsonReachability status per CVE/findingJSON

Ground-Truth Corpus Bundle

For validation and regression testing of reachability analysis:

/offline/corpus/ground-truth-v1.tar.zst
  ├── corpus-manifest.json        # Corpus metadata and sample count
  ├── dotnet/                     # .NET test cases (10 samples)
  │   ├── sample-001/
  │   │   ├── artifact.tar.gz     # Source/binary artifact
  │   │   ├── expected.json       # Ground-truth reachability verdicts
  │   │   └── callgraph.json      # Expected call-graph
  │   └── ...
  └── java/                       # Java test cases (10 samples)
      ├── sample-001/
      └── ...

Corpus validation:

stella scan validate-corpus --corpus /offline/corpus/ground-truth-v1.tar.zst

Expected output:

Proof Replay in Air-Gap Mode

To replay a scan with frozen feeds:

# Import the reachability bundle
stella admin import-reachability-bundle /offline/reachability/<scan-id>/

# Replay the score calculation
stella score replay --scan <scan-id> --verify-proof

# Expected: "Proof root hash matches: <hash>"

The replay command:

  1. Loads the frozen Concelier/Excititor snapshots from the manifest
  2. Re-executes scoring with the same inputs
  3. Computes a new proof root hash
  4. Verifies it matches the original (bit-identical determinism)

CLI Commands for Reachability

# Extract call-graph from artifact
stella scan graph --lang dotnet --sln /path/to/solution.sln --output callgraph.json

# Run reachability analysis
stella scan reachability --callgraph callgraph.json --sbom sbom.json --output reachability.json

# Package for offline transfer
stella scan export-bundle --scan <scan-id> --output /offline/reachability/<scan-id>/

2.3 · Provcache Air-Gap Integration

The Provenance Cache (Provcache) supports air-gapped environments through minimal proof bundles with lazy evidence fetching.

Proof Bundle Density Levels

DensityContentsTypical SizeAir-Gap Usage
LiteDecisionDigest + ProofRoot + Manifest~2 KBRequires lazy fetch for evidence
Standard+ First ~10% of evidence chunks~200 KBPartial evidence, lazy fetch remaining
Strict+ All evidence chunksVariableFull compliance, no network needed

Export Workflow

# Export lite bundle for minimal transfer size
stella prov export --verikey sha256:<key> --density lite --output proof-lite.json

# Export standard bundle (balanced)
stella prov export --verikey sha256:<key> --density standard --output proof-std.json

# Export strict bundle with full evidence + signature
stella prov export --verikey sha256:<key> --density strict --sign --output proof-full.json

Evidence Chunk Export for Sneakernet

For fully air-gapped environments using lite/standard bundles:

# Export all evidence chunks to directory for transport
stella prov export-chunks --proof-root sha256:<root> --output /mnt/usb/evidence/

# Output structure:
/mnt/usb/evidence/
  ├── sha256-<proof_root>/
  │   ├── manifest.json
  │   ├── 00000000.chunk
  │   ├── 00000001.chunk
  │   └── ...

Import Workflow on Air-Gapped Host

# Import with lazy fetch from file directory (sneakernet)
stella prov import proof-lite.json --lazy-fetch --chunks-dir /mnt/usb/evidence/

# Import with lazy fetch from local server (isolated network)
stella prov import proof-lite.json --lazy-fetch --backend http://provcache-server:8080

# Import strict bundle (no network needed)
stella prov import proof-full.json --verify

Programmatic Lazy Fetch

// File-based fetcher for air-gapped environments
var fileFetcher = new FileChunkFetcher(
    basePath: "/mnt/usb/evidence",
    logger);

var orchestrator = new LazyFetchOrchestrator(repository, logger);

// Fetch and verify all missing chunks
var result = await orchestrator.FetchAndStoreAsync(
    proofRoot: "sha256:...",
    fileFetcher,
    new LazyFetchOptions
    {
        VerifyOnFetch = true,
        BatchSize = 100
    });

if (result.Success)
    Console.WriteLine($"Fetched {result.ChunksStored} chunks");

Bundle Format (v1)

{
    "version": "v1",
    "exportedAt": "2025-01-15T10:30:00Z",
    "density": "standard",
    "digest": {
        "veriKey": "sha256:...",
        "verdictHash": "sha256:...",
        "proofRoot": "sha256:...",
        "trustScore": 85
    },
    "manifest": {
        "proofRoot": "sha256:...",
        "totalChunks": 42,
        "totalSize": 2752512,
        "chunks": [...]
    },
    "chunks": [...],
    "signature": {
        "algorithm": "ECDSA-P256",
        "signature": "base64...",
        "signedAt": "2025-01-15T10:30:01Z"
    }
}

2.3.1 · Compact vulnerability database (vuln-db.sqlite) — two-artifact structure & manifest

Since 2026-07 the compact offline vulnerability DB ships as a license-segregated artifact pair (counsel directive: docs/legal/decisions/compact-vulndb-license-segregation-counsel-thread-reply.md):

<db-version>/
  ├── vuln-db.sqlite                     # BUSL-core: license-cleared sources only
  ├── vuln-db.manifest.json              # sidecar manifest for the core artifact
  ├── vuln-db-sharealike.sqlite          # OPTIONAL companion: CC-BY-SA / share-alike sources,
  │                                      #   per-record attribution (record_attribution table)
  └── vuln-db-sharealike.manifest.json   # sidecar manifest for the companion

2.4 · Async federation export

Concelier’s federation bundle export now runs as a scheduled job. Air-gap operators kick off an export and pick up the artefact from S3/RustFS when the platform’s notification surface signals completion.

StepAction
1POST /api/v1/federation/exports with { sinceCursor?, maxItems?, compressLevel?, sign? }. Returns 202 Accepted + { exportId, runId, status: "queued" } and a Location: /api/v1/federation/exports/{exportId} header.
2The federation:bundle:export job streams the zstd bundle directly to S3/RustFS (feedser-bundle/{tenant}/{yyyymmdd}/{sha256}.zst).
3On terminal status, Concelier emits concelier.federation.bundle.ready or .failed to Notify; the requester receives an in-app inbox row plus an email (template seeded per tenant). Both carry a 24-hour pre-signed download URL.
4Poll GET /api/v1/federation/exports/{exportId} (or open the in-app notification) to retrieve a fresh signed URL after expiry.

The synchronous GET /api/v1/federation/export and GET /api/v1/federation/export/preview endpoints are deprecated; they remain only as a short-circuit for small (≤10k canonicals) exports. New air-gap pipelines should adopt the async surface so exports survive HTTP timeouts and are reproducibly content- addressed.

Design details: docs-archive/implplan/SPRINT_20260513_002_Concelier_async_export_s3_notify.md.

2.4.1 Advisory transport readiness

Routine air-gap advisory transport is the feedser-bundle/1.0 federation export. stellaops-mirror-seed-v1 (PostgreSQL COPY chunks) is reclassified as disaster-recovery / replica-bootstrap only — it is unfit for routine air-gap installs (6 GB compressed -> ~200 GB expanded -> 50+ h COPY import vs. the bundle’s ~tens-of-MB single pull applied in seconds).

PathCurrent useReadiness
feedser-bundle/1.0 federation exportRoutine advisory synchronization between connected and isolated Concelier instances.Production-ready (routine path). The full export/import pipeline is proven streaming + flat-working-set (FederationExportWorkingSetTests: 141 753 canonicals through the real pipeline, ~1.3 s / ~4 MB peak heap, keyset-only paging) and the populated-DB primary→replica round-trip is proven against real PostgreSQL backends (FederationPostgresRoundTripTests, Sprint 20260513_001 E0): a populated primary Postgres exports the bundle and a fresh replica Postgres imports it with row-count parity verified via raw SQL (active/withdrawn/edge counts + multi-source shape). The smaller live federation E2E proof is tracked in SPRINT_20260514_001_AirGap_federation_e2e_tiers.
json:raw / json:policyEvidence escrow, downstream normalization, and operator-readable advisory archives.Available after the JSON live proof in SPRINT_20260513_001_Concelier_airgap_export_pipeline_gaps E1; keep the format compatible with Export Center profiles.
trivy:dbScanner-feed transport for Trivy consumers.Available (clean-room BoltDB writer). SPRINT_20260513_001_Concelier_airgap_export_pipeline_gaps E2 landed an in-repo Trivy-compatible BoltDB writer (trivy.db + metadata.json) with a per-source license allowlist that segregates CC-BY-SA (Ubuntu) into a separate trivy-by-sa.db shard; black-box round-trip verified.
Thin bundleStrategic removable-media bundle owned by Mirror Thin Bundle Assembler.Strategic path from MIRROR-CRT-56-001; no sprint currently documents the first end-to-end “bundle on USB -> air-gap import in <5 min” demo.
stellaops-mirror-seed-v1PostgreSQL COPY seed for disaster recovery or replica bootstrap.DR / replication only; do NOT use as the routine update transport.

This classification is captured in ADR-003: Concelier Export Center Air-Gap Alignment.

For local development, devops/compose/docker-compose.dev.yml brings up a Mailpit container (mail.stella-ops.local:1025 SMTP, :8025 web UI) so the notification emails can be inspected end-to-end without a real SMTP relay. Apply the docker-compose.notifier-mailpit.override.yml override to point the Notifier worker’s DefaultSmtpOptions fall-back at the Mailpit instance via env vars (NOTIFIER__EMAIL__SMTP__HOST=mail.stella-ops.local, NOTIFIER__EMAIL__SMTP__PORT=1025). Full walkthrough lives in docs/modules/notifier/operations/dev-smtp.md.


3 · Delta patch workflow

  1. Connected site fetches stella-ouk-YYYY-MM-DD.delta.tgz.
  2. Transfer via any medium (USB, portable disk).
  3. stella admin import-offline-usage-kit <delta> applies only changed CVE rows & images.

Daily deltas are < 30 MB; weekly roll-up produces a fresh full kit.


4 · Quota behaviour offline

The scanner enforces the same fair-use limits offline:

Soft reminder at 200 scans; throttle above the ceiling but never block. See the quota enforcement flow in QUOTA_ENFORCEMENT_FLOW.md.


5 · Troubleshooting

SymptomExplanationFix
could not verify SBOM hashBundle corrupted in transitRe-download / re-copy
Import hangs at Applying feeds…Low disk space in /var/lib/stellaFree ≥ 2 GiB before retry
quota exceeded same day after importImport resets counters at UTC 00:00 onlyWait until next UTC day or load a JWT