Air-Gap Operations Runbook

Version: 1.1.2 Sprint: 3500.0004.0004 Last Updated: 2026-08-27

Audience: operators running Stella Ops in air-gapped (offline) environments. This runbook covers operational procedures for offline kit management, feed updates, scanning, and isolated verification workflows.

Read this banner first. Much of the CLI and configuration shown below predates the shipped code. The reconciliation note immediately following enumerates what is actually wired today; each section is annotated inline as implemented, Draft/roadmap, or NOT IMPLEMENTED. Use the CLI Quick Reference as the source of truth for runnable commands.

Implementation status (reconciled against src/ 2026-05-31). Large parts of this runbook describe an aspirational stella offline-kit … CLI and a generic mode: air-gap configuration model that do not exist in the codebase. The commands and config that are actually wired today are narrower:

  • CLI: the live command groups are stella airgap (subcommands import, seal, export-evidence) and stella offline (subcommands import, status). There is no stella offline-kit, stella feeds age, stella config set mode, stella health check --air-gap, stella score replay, or stella proof verify command on the air-gap path. A richer airgap group (export/import/diff/status/jobs) exists in src/Cli/StellaOps.Cli/Commands/AirGapCommandGroup.cs but is not registered in the CLI root (CommandFactory), so it is not reachable from stella today.
  • HTTP API (AirGap Controller, src/AirGap/StellaOps.AirGap.Controller): GET /system/airgap/status (scope airgap:status:read), POST /system/airgap/seal and POST /system/airgap/unseal (scope airgap:seal), POST /system/airgap/verify (scope airgap:status:read). Tenant is taken from the JWT stellaops:tenant claim (with an optional matching x-tenant-id header). Scope note: the verify endpoint’s AirGap.Verify policy requires StellaOpsScopes.AirgapStatusRead (Program.cs:59-69) — verification is a read-only integrity check, so it shares the air-gap read scope rather than a distinct one. There is no airgap:verify scope; the canonical catalog (StellaOpsScopes.cs) defines exactly airgap:seal, airgap:import, and airgap:status:read. Since airgap:status:read is already granted to the console and CLI clients, POST /system/airgap/verify is reachable with an ordinary OpTok. Note also that airgap:import is registered as a scope but the controller’s AirGap.Import policy is not bound to any controller route.
  • HTTP API (AirGap Time, src/AirGap/StellaOps.AirGap.Time): GET /api/v1/time/status (scope airgap:status:read), POST /api/v1/time/anchor (scope airgap:seal — there is no dedicated anchor-write scope; AirGapTimePolicies.AnchorWriteScope reuses airgap:seal), readiness at /healthz/ready. Tenant is taken from the JWT stellaops:tenant claim; a tenantId query/body value, if present, must match the claim.
  • Egress model: sealing is an egress policy with two modes — Unsealed (advisory) and Sealed (block by allow-rule) — see src/AirGap/StellaOps.AirGap.Policy. There is no disableNetworking flag.

Inline annotations below mark each block as implemented, Draft/roadmap, or NOT IMPLEMENTED so operators do not run commands that will fail.

Q-25 ownership update (owner 2026-08-27). OfflineKit is the sole canonical HTTP import/status/manifest/validate owner at /api/offlinekit/v1. It owns verified carrier handling, quarantine/CAS, bundle lifecycle and its import ledger. Payload activation stays with the data owner: Vulnerabilities for vulnerability/VEX parts and Scanner for Scanner-owned parts. The exact Scanner artifact-BOM part now crosses Scanner’s bounded owner API and records per-part quarantined -> staged -> activated|failed state. Current status/manifest still report carrier-level verified custody plus manifest inventory, while parts[] reports independent domain activation; aggregate contentActivated remains false. Vulnerabilities/VEX and generic adapters remain engineering work. The settlement performed no live deployment, route or data cutover. The CLI-local stella airgap import path remains distinct and must not be reported deleted by Q-25.


Table of Contents

  1. Overview
  2. Offline Kit Management
  3. Feed Updates
  4. Scanning in Air-Gap Mode
  5. Verification in Air-Gap Mode
  6. Troubleshooting
  7. Monitoring & Health Checks
  8. Escalation Procedures

1. Overview

What is Air-Gap Mode?

Air-gap mode allows StellaOps to operate in environments with no external network connectivity. This is required for:

Air-Gap Architecture

┌─────────────────────────────────────────────────────────────┐
│ Connected Environment │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Feed Sync │───►│ Bundle │───►│ Offline Kit │ │
│ │ Service │ │ Generator │ │ (.tar.gz) │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
└─────────────────────────────────────────────────┼───────────┘
 │ Physical
 │ Transfer
┌─────────────────────────────────────────────────┼───────────┐
│ Air-Gapped Environment │ │
│ ┌──────────────┐ ┌─────────────┐ ┌──────▼──────┐ │
│ │ StellaOps │◄───│ Offline │◄───│ Import │ │
│ │ Scanner │ │ Data Store │ │ Service │ │
│ └──────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘

Offline Kit Contents

ComponentDescriptionUpdate Frequency
Vulnerability DatabaseNVD, OSV, vendor advisoriesDaily/Weekly
Advisory FeedsCVE details, EPSS scoresDaily
Trust BundlesCA certificates, signing keysQuarterly
Rules EngineScoring rules and policiesMonthly
Offline BinariesCLI tools, extractorsPer release

2. Offline Kit Management

Mixed implementation status — distinguish three command paths. The flat stella offline-kit … family illustrated below (create, list, verify, rollback, snapshots, days-until-expiry) does not exist. The nested backend client group stella offline kit import|status|pull exists and Q-25 assigns its HTTP import/status callers to OfflineKit’s canonical /api/offlinekit/v1 surface. stella offline import --bundle <path.tar.zst> is a CLI-local OfflineCommandGroup activation path, while stella airgap import --bundle <dir> uses the separate live MirrorBundleImportService handler. Q-25 does not delete either local mechanism; their convergence/disposition remains OK-7 engineering work. The snippets in 2.1–2.4 are conceptual and must be mapped to the CLI Quick Reference before use.

The Console’s read-only status surface is implemented at /ops/operations/offline-kit. Under Q-25 its Dashboard reads OfflineKit’s tenant-scoped /api/offlinekit/v1/status and /api/offlinekit/v1/manifest contracts and shows a bounded loading, honest empty, or explicit error state; it does not synthesize bundle or activity data in the browser. Repointing source is not live proof: until the owner-attended cutover succeeds, the deployed Console may still be backed by Scanner’s old route.

2.1 Generating an Offline Kit

On the connected system:

# Generate full offline kit
stella offline-kit create \
 --output /path/to/offline-kit.tar.gz \
 --include-all

# Generate minimal kit (feeds only)
stella offline-kit create \
 --output /path/to/offline-kit-feeds.tar.gz \
 --feeds-only

# Generate with specific components
stella offline-kit create \
 --output /path/to/offline-kit.tar.gz \
 --include vuln-db \
 --include advisories \
 --include trust-bundles \
 --include rules

2.2 Kit Manifest

Each kit includes a manifest for verification:

{
 "version": "1.0.0",
 "createdAt": "2025-01-15T00:00:00Z",
 "expiresAt": "2025-02-15T00:00:00Z",
 "components": {
 "vulnerability-database": {
 "hash": "sha256:abc123...",
 "size": 1073741824,
 "records": 245000
 },
 "advisory-feeds": {
 "hash": "sha256:def456...",
 "size": 536870912,
 "lastUpdate": "2025-01-15T00:00:00Z"
 },
 "trust-bundles": {
 "hash": "sha256:ghi789...",
 "size": 65536,
 "certificates": 12
 },
 "signing-keys": {
 "hash": "sha256:jkl012...",
 "keyIds": ["key-001", "key-002"]
 }
 },
 "signature": "base64-signature..."
}

2.3 Transferring to Air-Gapped Environment

Physical Media Transfer

# On connected system - write to media
cp offline-kit.tar.gz /media/secure-usb/
sha256sum offline-kit.tar.gz > /media/secure-usb/offline-kit.tar.gz.sha256

# On air-gapped system - verify and import
cd /media/secure-usb/
sha256sum -c offline-kit.tar.gz.sha256
stella offline-kit import --kit offline-kit.tar.gz

Secure File Transfer (if available)

# Using data diode or one-way transfer
scp offline-kit.tar.gz airgap-gateway:/incoming/

2.4 Installing Offline Kit

# Import and install kit
stella offline-kit import \
 --kit /path/to/offline-kit.tar.gz \
 --verify \
 --install

# Verify installation
stella offline-kit status

# List installed components
stella offline-kit list

Domain activation after Q-25. Do not send vulnerability/advisory/VEX content to the retired Concelier/Excititor runtime plane and do not run its database procedures from OfflineKit. Current OfflineKit source verifies a strict in-toto carrier statement binding both uploaded bundle and manifest digests plus an OfflineKit-specific DSSE envelope over the exact manifest bytes, stores verified carrier custody, and keeps domain activation separate from that custody result. For the exact Scanner artifact-BOM declaration it extracts and verifies the referenced part, calls Scanner’s bounded owner API, and records quarantined -> staged -> activated|failed state. A future Vulnerabilities adapter will activate vulnerability/advisory/VEX content. The generic media-type registry and remaining adapters are not yet implemented; the Scanner path is not yet live-accepted.

Canonical API status shape (source contract; not live acceptance):

{
  "latestVerifiedCarrier": {
    "bundleId": null,
    "channel": "<channel>",
    "kind": null,
    "isDelta": null,
    "baseBundleId": null,
    "bundleSha256": "sha256:<digest>",
    "bundleSize": 0,
    "capturedAt": "<timestamp-or-null>",
    "verifiedAt": "<timestamp>"
  },
  "custodyStatus": "verified",
  "contentActivated": false,
  "manifestArtifacts": [
    {
      "path": "scanner/artifact-boms.v1.json",
      "digest": "sha256:<digest>",
      "sizeBytes": 0
    }
  ],
  "parts": [
    {
      "path": "scanner/artifact-boms.v1.json",
      "domain": "scanner",
      "state": "activated",
      "failureCode": null,
      "updatedAt": "<timestamp>"
    }
  ]
}

manifestArtifacts are signed {path,digest,sizeBytes} declarations projected from the verified builder artifacts[] array; they do not assert activation. parts[] is the activation authority for handled declarations. The builder currently signs no bundle.bundleId, bundle.kind or bundle.isDelta, so those values are null; consumers must not substitute bundle.version or unsigned multipart metadata. The API exposes no current, components, manifestComponents or importedAt aliases, and the CLI/Console must not render the artifact declarations as installed or active content without a matching activated part. GET /api/offlinekit/v1/manifest returns the exact verified raw manifest JSON, or 204 when no verified carrier exists.


3. Feed Updates

NOT IMPLEMENTED — illustrative CLI. stella offline-kit create --delta-from/--delta-to, stella offline-kit snapshots, and stella offline-kit rollback have no backing command. stella feeds status resolves to the deterministic-replay feed-snapshot group (src/Cli/StellaOps.Cli/Commands/FeedsCommandGroup.cs) / admin feeds group, not an air-gap freshness reporter. Delta kits and snapshot rollback are Draft/roadmap; the supported recovery action today is re-importing the last known-good bundle (see docs/modules/airgap/guides/operations.md “Failure recovery”).

3.1 Update Workflow

┌────────────────────────────────────────────────────────────┐
│ Weekly Feed Update Process │
├────────────────────────────────────────────────────────────┤
│ Day 1: Generate kit on connected system │
│ Day 2: Security review and approval │
│ Day 3: Transfer to air-gapped environment │
│ Day 4: Import and verify │
│ Day 5: Activate new feeds │
└────────────────────────────────────────────────────────────┘

3.2 Generating Delta Updates

For faster updates, generate delta kits:

# On connected system
stella offline-kit create \
 --output delta-kit.tar.gz \
 --delta-from 2025-01-08 \
 --delta-to 2025-01-15

# Delta kit is smaller, contains only changes

3.3 Applying Updates

# Import delta update
stella offline-kit import \
 --kit delta-kit.tar.gz \
 --delta \
 --verify

# Verify feed freshness
stella feeds status

3.4 Rollback Procedure

If an update causes issues:

# List available snapshots
stella offline-kit snapshots

# Rollback to previous version
stella offline-kit rollback --to 2025-01-08

# Verify rollback
stella feeds status

4. Scanning in Air-Gap Mode

NOT IMPLEMENTED as written — see notes. There is no global stella config set mode air-gap toggle and no stella config get mode that returns air-gap. Sealing is enforced per-process through the egress policy (EgressPolicyMode = Unsealed/Sealed, src/AirGap/StellaOps.AirGap.Policy) and, for the controller, via POST /system/airgap/seal. Scan invocation flags shown below (stella scan image --local, --offline-feeds) are Draft/roadmap for the air-gap surface; the live scan command group is src/Cli/StellaOps.Cli/Commands/CommandFactory.cs (scan) — verify its actual flags before use.

4.1 Enabling Air-Gap Mode

# Enable air-gap mode
stella config set mode air-gap

# Verify mode
stella config get mode
# Output: air-gap

4.2 Running Scans

# Scan a local image (no registry pull)
stella scan image --local /path/to/image.tar

# Scan from local registry
stella scan image localhost:5000/myapp:v1.0

# Scan with offline feeds explicitly
stella scan image myapp:v1.0 --offline-feeds

4.3 Local Image Preparation

For images that need to be scanned:

# On connected system - save image
docker save myapp:v1.0 -o myapp-v1.0.tar
sha256sum myapp-v1.0.tar > myapp-v1.0.tar.sha256

# Transfer to air-gapped system
#... physical transfer...

# On air-gapped system - verify and load
sha256sum -c myapp-v1.0.tar.sha256
docker load -i myapp-v1.0.tar
stella scan image myapp:v1.0 --local

4.4 SBOM Generation

# Generate SBOM for local image
stella sbom generate --image myapp:v1.0 --output sbom.json

# Scan existing SBOM
stella scan sbom --file sbom.json

5. Verification in Air-Gap Mode

Mixed implementation. Offline verification commands exist but under different names/groups than shown here, and the closest matches differ in how “real” they are:

  • stella verify offline(VerifyCommandGroup, registered) — live with a real handler. This is the actual offline-evidence verification command.
  • stella score verify(the score command is built by ScoreReplayCommandGroup.BuildScoreCommand, registered — not the unregistered ScoreCommandGroup) — live; calls a real score-bundle verify endpoint.
  • stella evidence proof / stella evidence replay score(EvidenceCommandGroup, registered) — present but the subcommands are stub placeholders that print canned Console.WriteLine output and return 0 without verifying anything. There is no stella evidence proof verify subcommand (the proof group only has generate/anchor/receipt). Do not rely on these for real verification yet.
  • stella trust …(Commands/Trust/TrustCommandGroup.cs) — NOT reachable: BuildTrustCommand is defined but is not registered in the CLI root (CommandFactory); it is only wired up inside a unit test. So stella trust verify, stella trust export, and stella trust import are all unreachable today. (For the record, the defined-but-unwired group does contain init/sync/status/verify/export/import/snapshot export TUF operations — so export/import exist in code, they just are not exposed.)

The specific spellings below — stella proof verify, stella score replay --offline, stella trust export, stella trust import — do not all map to live commands. Treat 5.1–5.3 as Draft/roadmap spellings and confirm against stella <group> --help.

5.1 Offline Proof Verification

# Verify proof bundle offline
stella proof verify --bundle bundle.tar.gz --offline

# Verify with explicit trust store
stella proof verify --bundle bundle.tar.gz \
 --offline \
 --trust-store /etc/stellaops/offline/trust-roots.json

5.2 Preparing Trust Store

Before air-gapped deployment:

# On connected system - export trust configuration
stella trust export \
 --output trust-roots.json \
 --include-ca \
 --include-signing-keys

# Transfer to air-gapped system
#... physical transfer...

# On air-gapped system - import trust
stella trust import --file trust-roots.json

5.3 Score Replay Offline

# Replay score using offline data
stella score replay --scan $SCAN_ID --offline

# Replay with frozen time
stella score replay --scan $SCAN_ID \
 --offline \
 --freeze 2025-01-15T00:00:00Z

6. Troubleshooting

CLI spellings are illustrative. The diagnostic commands in this section (stella offline-kit verify, stella feeds status, stella trust list, stella trust check-expiry, stella config get mode) follow the aspirational CLI and are NOT IMPLEMENTED as written. The diagnostic intent is sound; map to live commands (re-verified 2026-09-14 against the rebuilt CLI): carrier verification without admission → POST /api/offlinekit/v1/validate with the manifest and its DSSE envelope (stella airgap import has no --verify-only; it admits or refuses, exit 0 / 2 — see docs/runbooks/airgap/offlinekit-import.md); offline-kit validation → stella offline import --bundle <path> --dry-run (validates without activating, DSSE/Rekor checks on by default); air-gap posture → stella airgap status --scope installation (reads Platform’s airgap-seal environment state; GET /system/airgap/status died with the controller host); time-anchor → stella airgap time-anchor status. The openssl verify … and journalctl/grep steps are generic OS tooling and remain valid.

6.1 Kit Import Fails

Symptoms: Failed to import offline kit

Diagnostic Steps:

  1. Verify kit integrity:
sha256sum offline-kit.tar.gz
# Compare with manifest
  1. Check kit signature:
stella offline-kit verify --kit offline-kit.tar.gz
  1. Check disk space:
df -h /var/lib/stellaops/

Common Causes:

CauseResolution
Corrupted transferRe-transfer, verify checksum
Invalid signatureRegenerate kit with valid signing key
Insufficient spaceFree disk space or expand volume
Expired kitGenerate fresh kit

6.2 Stale Feed Data

Symptoms: Scans report old vulnerabilities, miss new CVEs

Diagnostic Steps:

  1. Check feed age:
stella feeds status
  1. Verify last update:
stella offline-kit status | grep "Installed At"

Resolution:

6.3 Trust Verification Fails

Symptoms: Certificate chain verification failed in offline mode

Diagnostic Steps:

  1. Check trust store:
stella trust list
  1. Verify CA bundle:
openssl verify -CAfile /etc/stellaops/offline/ca-bundle.pem \
/path/to/certificate.pem
  1. Check for expired roots:
stella trust check-expiry

Resolution:

6.4 Network Access Attempted

Symptoms: Air-gapped system attempts network connection

Diagnostic Steps:

  1. Check mode configuration:
stella config get mode
  1. Audit network attempts:
journalctl -u stellaops | grep -i "network\|connect\|http"
  1. Verify no external URLs in config:
grep -r "http" /etc/stellaops/

Resolution:


7. Monitoring & Health Checks

stella health check --air-gap is NOT IMPLEMENTED. No CLI health subcommand emits the consolidated air-gap report shown in 7.1, and the automated script in 7.2 calls non-existent commands (stella feeds age --days, stella offline-kit days-until-expiry, stella trust verify). Service readiness is exposed at /healthz/ready (AirGap Time) and air-gap state at GET /system/airgap/status. The actual emitted metrics are enumerated in 7.3 (corrected); the example script and alert YAML in 7.2/7.4 are Draft/roadmap illustrations.

7.1 Air-Gap Health Checks

# Run comprehensive health check
stella health check --air-gap

# Output
Air-Gap Health Check
══════════════════════════════════════════
✓ Mode: air-gap
✓ Feed Freshness: 7 days old (OK)
✓ Trust Store: Valid
✓ Signing Keys: 2 active
✓ Disk Space: 45% used
✓ Database: Healthy
⚠ Kit Expiry: 24 days remaining

Overall: HEALTHY (1 warning)

7.2 Automated Monitoring Script

#!/bin/bash
# /etc/stellaops/scripts/airgap-health.sh

set -e

# Check feed age
FEED_AGE=$(stella feeds age --days)
if [ "$FEED_AGE" -gt 14 ]; then
 echo "CRITICAL: Feeds are $FEED_AGE days old"
 exit 2
fi

# Check kit expiry
DAYS_LEFT=$(stella offline-kit days-until-expiry)
if [ "$DAYS_LEFT" -lt 7 ]; then
 echo "WARNING: Kit expires in $DAYS_LEFT days"
 exit 1
fi

# Check trust store
if ! stella trust verify --quiet; then
 echo "CRITICAL: Trust store verification failed"
 exit 2
fi

echo "OK: Air-gap health check passed"
exit 0

7.3 Metrics for Air-Gap

Corrected to match emitted instruments. The previously listed metric names (offline_kit_age_days, offline_kit_expiry_days, feed_freshness_days, trust_store_valid, disk_usage_percent) are NOT emitted by any service in src/. The real instruments are below, sourced from the three air-gap meters. Note the units: ages/budgets are reported in seconds, not days, so alert thresholds must be expressed in seconds.

Meter StellaOps.AirGap.Controller(AirGapTelemetry.cs):

MetricTypeDescription
airgap_seal_totalcounterSeal operations (tagged tenant, sealed)
airgap_unseal_totalcounterUnseal operations (tagged tenant, sealed)
airgap_startup_blocked_totalcounterSealed-startup validation failures (tagged tenant, reason)
airgap_time_anchor_age_secondsobservable gaugeAge of the time anchor, per tenant
airgap_staleness_budget_secondsobservable gaugeConfigured staleness breach budget, per tenant

Meter StellaOps.AirGap.Importer(OfflineKitMetrics.cs):

MetricTypeDescription
offlinekit_import_totalcounterOffline-kit import attempts (tagged status, tenant_id)
offlinekit_attestation_verify_latency_secondshistogramAttestation verification latency
attestor_rekor_success_totalcounterSuccessful Rekor verifications
attestor_rekor_retry_totalcounterRekor verification retries
rekor_inclusion_latencyhistogramRekor inclusion-proof verification time

Withdrawn 2026-09-14 (SPRINT_20260914_002 EA-8, D-EA4-1): the StellaOps.AirGap.Sync library, its meter and the stella airgap export / airgap jobs handlers are frozen under src/__Obsoleted/OfflineKit/__Libraries/StellaOps.AirGap.Sync/ and src/__Obsoleted/Cli/StellaOps.Cli/Commands/AirGapSync/. Nothing emits these instruments; the table stays as the record of what the predecessor declared.

Meter StellaOps.AirGap.Sync(HLC job sync — AirGapSyncMetrics.Counters.cs). The full instrument set is:

MetricTypeDescription
airgap_bundles_exported_totalcounterAir-gap bundles exported
airgap_bundles_imported_totalcounterAir-gap bundles imported
airgap_jobs_synced_totalcounterJobs synced from air-gap bundles
airgap_duplicates_dropped_totalcounterDuplicate entries dropped during merge
airgap_merge_conflicts_totalcounterMerge conflicts by type
airgap_offline_enqueues_totalcounterOffline enqueue operations
airgap_bundle_size_byteshistogramAir-gap bundle size in bytes
airgap_sync_duration_secondshistogramAir-gap sync operation duration
airgap_merge_entries_counthistogramEntry count per merge operation

The condensed table in Appendix D Monitoring Metrics lists only the five most operationally relevant of these.

7.4 Alert Configuration

# /etc/stellaops/alerts/airgap.yaml
alerts:
 - name: offline_kit_expiring
 condition: offline_kit_expiry_days < 7
 severity: warning
 message: "Offline kit expires in {{.Value }} days"

 - name: offline_kit_expired
 condition: offline_kit_expiry_days <= 0
 severity: critical
 message: "Offline kit has expired"

 - name: feeds_stale
 condition: feed_freshness_days > 14
 severity: warning
 message: "Vulnerability feeds are {{.Value }} days old"

8. Escalation Procedures

8.1 Escalation Matrix

SeverityConditionResponse TimeAction
P1 - CriticalOffline kit expired4 hoursEmergency kit transfer
P1 - CriticalTrust store invalid4 hoursRestore from backup
P2 - HighFeeds > 14 days old24 hoursSchedule kit update
P3 - MediumKit expiring in < 7 days48 hoursPlan kit update
P4 - LowMinor health check warningsNext maintenanceReview and address

8.2 Emergency Kit Update Process

When kit expires before scheduled update:

  1. On Connected System (0-2 hours):
stella offline-kit create --output emergency-kit.tar.gz --include-all
  1. Security Review (2-4 hours):
  1. Transfer (4-6 hours):
  1. Import (6-8 hours):
stella offline-kit import --kit emergency-kit.tar.gz --verify --install

8.3 Contacts

RoleContactAvailability
Air-Gap Operationsairgap-ops@stellaops.ioBusiness hours
Security Teamsecurity@stellaops.ioBusiness hours
Platform On-Callplatform-oncall@stellaops.io24/7

Appendix A: Configuration Reference

Draft/roadmap — does not match live config keys. No service binds a top-level mode: air-gap, an offline: block (dataDir, feedsDir, trustStore, caBundle, disableNetworking, kit:, feeds:), or the STELLAOPS_MODE / STELLAOPS_DISABLE_NETWORKING environment variables. The real sealing config is the egress policy section consumed by src/AirGap/StellaOps.AirGap.Policy:

egressPolicy: # bound via AddEgressPolicy(...); section name is host-defined
mode: Sealed # Unsealed (advisory) | Sealed (block unless allow-ruled)
allowLoopback: true
allowPrivateNetworks: false
allowRules:
- hostPattern: registry.internal
port: 5000
transport: Any
remediationDocumentationUrl: https://…
supportContact: airgap-ops@…

AirGap Time options (AirGapOptions, src/AirGap/StellaOps.AirGap.Time/Models): TenantId, Staleness.{WarningSeconds,BreachSeconds}, per-content ContentBudgets (advisories/vex/policy), TrustRootFile, AllowUntrustedAnchors, AllowRequestSuppliedTrustRoots. The YAML below is retained only as an aspirational example.

Air-Gap Mode Configuration

# /etc/stellaops/config.yaml
mode: air-gap

offline:
 dataDir: /var/lib/stellaops/offline
 feedsDir: /var/lib/stellaops/offline/feeds
 trustStore: /etc/stellaops/offline/trust-roots.json
 caBundle: /etc/stellaops/offline/ca-bundle.pem
 
 # Disable all external network calls
 disableNetworking: true
 
 # Kit expiry settings
 kit:
 expiryWarningDays: 7
 maxAgeDays: 30
 
 # Feed freshness settings 
 feeds:
 maxAgeDays: 14
 warnAgeDays: 7

Environment Variables

# Air-gap specific
export STELLAOPS_MODE=air-gap
export STELLAOPS_OFFLINE_DATA_DIR=/var/lib/stellaops/offline
export STELLAOPS_DISABLE_NETWORKING=true

# Trust configuration
export STELLAOPS_TRUST_STORE=/etc/stellaops/offline/trust-roots.json
export STELLAOPS_CA_BUNDLE=/etc/stellaops/offline/ca-bundle.pem

Appendix B: CLI Quick Reference

Reconciled against src/Cli/StellaOps.Cli (registered commands only). The list below reflects what is actually reachable from the stella root command. Commands from The whole stella trust … group is defined in code but not registered in the CLI root, so it is unreachable today (see the “NOT reachable” note below).

# Air-gap environment ops (CommandFactory.BuildAirgapCommand)
stella airgap import --bundle <dir> # Import a mirror bundle into the local store
stella airgap import --bundle <dir> --verify-only # Verify integrity without importing
stella airgap seal [--reason <text>] [--scope <scope>] [--force] [--dry-run] # Declare posture SEALED in the Platform environment-state custodian (env-scoped, register item 15; the old local sealed.json marker is retired)
stella airgap export-evidence -o <dir> # Export portable evidence packages

# Offline kit import (OfflineCommandGroup)
stella offline import --bundle <path.tar.zst> # Import offline kit with DSSE/Rekor verification
stella offline import --bundle <path> --dry-run # Validate without activating
stella offline status # Show current offline kit status

# Verification (different groups; confirm with --help)
stella verify offline … # Offline evidence verification (VerifyCommandGroup) — REAL handler
stella score verify … # Verify a score bundle (score cmd built by ScoreReplayCommandGroup) — REAL handler
stella evidence proof generate|anchor|receipt # Evidence proof ops (EvidenceCommandGroup) — STUB placeholders, no real verification; NO `proof verify` subcommand
stella evidence replay score … # Score-replay verification (EvidenceCommandGroup) — STUB placeholder

# AirGap Controller HTTP API (no CLI wrapper today)
# GET /system/airgap/status
# POST /system/airgap/seal | /unseal | /verify
# AirGap Time HTTP API
# GET /api/v1/time/status | POST /api/v1/time/anchor | GET /healthz/ready

# NOT reachable from the `stella` root today:
# stella trust init|sync|status|verify|export|import|snapshot
# (TrustCommandGroup.BuildTrustCommand is defined but NOT registered in CommandFactory)
# NOT registered in the CLI root (exists in AirGapCommandGroup.cs but unwired):
# stella airgap export | diff | status | jobs export|import|list

Appendix C: Update Schedule Template

WeekActivityOwnerNotes
1Generate kitConnected OpsMonday
1Security reviewSecurity TeamTuesday
1ApprovalSecurity LeadWednesday
2TransferAir-Gap OpsMonday
2Import & verifyAir-Gap OpsTuesday
2ActivateAir-Gap OpsWednesday
2Validate scansQA TeamThursday

Revision History

VersionDateAuthorChanges
1.0.02025-12-20AgentInitial release
1.1.02026-01-07AgentAdded HLC job sync section
1.1.12026-05-30AgentDeep doc↔code reconciliation against src/AirGap, the scope catalog, and src/Cli. Added implementation-status banner; corrected the mirror import endpoint to POST /api/v1/advisory-sources/mirror/import and removed the bogus “mirror.md §8.4” reference; replaced the §7.3 metrics table with the actually-emitted instruments (Controller/Importer/Sync meters); rewrote the Appendix B CLI quick reference to the registered stella airgap / stella offline commands; annotated §§2–7 and Appendix A/D CLI and config as NOT IMPLEMENTED or Draft/roadmap.
1.1.22026-05-31AgentSecond reconciliation pass. Corrected the verification surface: stella trust … is defined (TrustCommandGroup) but not registered in CommandFactory, so the whole group (incl. trust verify/export/import) is unreachable — fixed §5 banner and Appendix B which previously listed it as live. Fixed Appendix B: there is no stella evidence proof verify (the proof group has only generate/anchor/receipt, all stub Console.WriteLine placeholders); flagged evidence proof/evidence replay score as stubs; attributed the live score verify to ScoreReplayCommandGroup (not ScoreCommandGroup). Fixed §6: stella offline import has no --verify-only (use --dry-run); only stella airgap import has --verify-only. Completeness: §7.3 now lists the full StellaOps.AirGap.Sync instrument set (added airgap_duplicates_dropped_total, airgap_offline_enqueues_total, airgap_bundle_size_bytes, airgap_merge_entries_count) and Appendix D cross-references it as condensed; documented the AirGap Time endpoint scopes (statusairgap:status:read, anchorairgap:seal).

Appendix D: HLC-Based Job Synchronization

Sprint: SPRINT_20260105_002_003_ROUTER Added: 2026-01-07

Status: WITHDRAWN 2026-09-14 (SPRINT_20260914_002 EA-8 on D-EA4-1: no production reader, no registered verb, no owner service). Sources frozen under src/__Obsoleted/OfflineKit/__Libraries/StellaOps.AirGap.Sync/; the rest of this note is the 2026-01-07 measurement of the predecessor. The HLC sync engine and its telemetry were real — see the frozen library and the command handlers in src/Cli/StellaOps.Cli/Commands/CommandHandlers.AirGap.cs. The Monitoring Metrics names below match the emitted StellaOps.AirGap.Sync meter exactly. However, the stella airgap export and stella airgap jobs export|import|list commands live in AirGapCommandGroup.cs, which is not registered in CommandFactory, so they are not reachable from the stella root today (only airgap import, airgap seal, airgap export-evidence are). The --conflict-strategy and stella airgap verify --bundle references in the troubleshooting subsection are likewise Draft/roadmap until the group is wired in.

Overview

When running in air-gap mode, scheduled jobs are queued locally using Hybrid Logical Clocks (HLC). Upon reconnection or physical transfer, these jobs can be merged deterministically with the central scheduler while preserving global ordering.

Key Concepts

TermDescription
HLC TimestampTuple of (PhysicalTime, LogicalCounter, NodeId) for total ordering
Job LogAppend-only log of jobs enqueued while offline
Chain LinkCryptographic hash linking each job to its predecessor
Air-Gap BundleExportable package containing job logs from one or more offline nodes

Exporting Jobs from Offline Node

# Export all pending jobs to a bundle
stella airgap export \
 --output /media/usb/jobs-bundle.json \
 --tenant <tenant-id>

# Export with DSSE signature (requires signing key)
stella airgap export \
 --output /media/usb/jobs-bundle.json \
 --tenant <tenant-id> \
 --sign

Importing Jobs to Central Scheduler

# Import bundle and merge jobs
stella airgap import \
 --bundle /media/usb/jobs-bundle.json

# Import with signature verification
stella airgap import \
 --bundle /media/usb/jobs-bundle.json \
 --verify-signature

# Dry-run to preview merge results
stella airgap import \
 --bundle /media/usb/jobs-bundle.json \
 --dry-run

Merge Behavior

  1. Total Ordering: Jobs are merged by HLC order key (PhysicalTime, LogicalCounter, NodeId, JobId)
  2. Duplicate Detection: Jobs with the same JobId are deduplicated
  3. Chain Verification: Each job’s chain link is verified against its predecessor
  4. Conflict Resolution: Same-payload jobs from different nodes are kept; conflicting payloads are flagged

Monitoring Metrics

These are the most operationally relevant instruments from the StellaOps.AirGap.Sync meter (AirGapSyncMetrics.Counters.cs). The full set — which also includes airgap_duplicates_dropped_total, airgap_offline_enqueues_total, airgap_bundle_size_bytes, and airgap_merge_entries_count — is enumerated in §7.3 Metrics for Air-Gap.

MetricDescription
airgap_bundles_exported_totalTotal bundles exported
airgap_bundles_imported_totalTotal bundles imported
airgap_jobs_synced_totalTotal jobs synchronized
airgap_merge_conflicts_totalMerge conflicts by type
airgap_sync_duration_secondsSync operation duration

Troubleshooting

Bundle signature verification fails

  1. Check that signing key is correctly configured
  2. Verify bundle was not modified during transfer
  3. Check key ID matches expected signer

Merge conflicts detected

  1. Review conflicts in import output
  2. Check for clock skew between offline nodes
  3. Use --conflict-strategy flag to auto-resolve

Jobs missing after import

  1. Check for duplicate JobIds (same job, same node)
  2. Verify chain integrity with stella airgap verify --bundle <path>
  3. Review import logs for skipped entries