# AirGap Controller The AirGap Controller is the tenant-scoped state keeper for sealed-mode operation. It records whether an installation is sealed, what policy hash is active, which time anchor is in force, and what staleness budgets apply. For workflow context, start at `docs/airgap/overview.md` and `docs/airgap/airgap-mode.md`. ## Responsibilities - Maintain the current AirGap state per tenant (sealed/unsealed, policy hash, time anchor, staleness budgets). - Provide a deterministic, auditable status snapshot for operators and automation. - Enforce sealed/unsealed transitions via Authority scopes. - Emit telemetry signals suitable for dashboards and forensics timelines. Non-goals: - Bundle signature validation and import staging (owned by the importer; see `docs/airgap/importer.md`). - Cryptographic signing (Signer/Attestor). ## API Base route group: `/system/airgap` (requires authorization). ### `GET /system/airgap/status` Required scope: `airgap:status:read` Response: `AirGapStatusResponse` (current state + staleness evaluation). Notes: - Tenant routing uses `x-tenant-id` (defaults to `default` if absent). - `driftSeconds` and `secondsRemaining` are derived from the active time anchor and staleness budget evaluation. - `contentStaleness` contains per-category staleness evaluations (clients should treat keys as case-insensitive). ### `POST /system/airgap/seal` Required scope: `airgap:seal` Body: `SealRequest` - `policyHash` (required): binds the sealed state to a specific policy revision. - `timeAnchor` (optional): time anchor record (from the AirGap Time service). - `stalenessBudget` (optional): default staleness budget. - `contentBudgets` (optional): per-category staleness budgets (e.g., `advisories`, `vex`, `scanner`). Behavior: - Rejects requests missing `policyHash` (`400 { \"error\": \"policy_hash_required\" }`). - Records the sealed state and returns an updated status snapshot. ### `POST /system/airgap/unseal` Required scope: `airgap:seal` Behavior: - Clears the sealed state and returns an updated status snapshot. - Staleness is returned as `Unknown` after unseal (clients should treat this as "not applicable"). ### `POST /system/airgap/verify` Required scope: `airgap:verify` Purpose: verify replay / bundle verification requests against the currently active AirGap state. ## State model (per tenant) Canonical fields captured by the controller (see `src/AirGap/StellaOps.AirGap.Controller`): - `tenantId` - `sealed` - `policyHash` (nullable) - `timeAnchor` (`TimeAnchor`, may be `Unknown`) - `stalenessBudget` (`StalenessBudget`) - `contentBudgets` (`Dictionary`) - `driftBaselineSeconds` (baseline used to keep drift evaluation stable across transitions) - `lastTransitionAt` (UTC) Determinism requirements: - Use UTC timestamps only. - Use ordinal comparisons for keys and stable serialization settings for JSON responses. - Never infer state from wall-clock behavior other than the injected `TimeProvider`. ## Telemetry The controller emits: - Structured logs: `airgap.status.read`, `airgap.sealed`, `airgap.unsealed`, `airgap.verify` (include `tenant_id`, `policy_hash`, and drift/staleness). - Metrics: `airgap_seal_total`, `airgap_unseal_total`, `airgap_status_read_total`, and gauges for drift/budget/remaining seconds. - Timeline events (optional): `airgap.sealed`, `airgap.unsealed`, `airgap.staleness.warning`, `airgap.staleness.breach`. ## References - `docs/airgap/overview.md` - `docs/airgap/sealed-startup-diagnostics.md` - `docs/airgap/staleness-and-time.md` - `docs/airgap/time-api.md` - `docs/airgap/importer.md`