Air-Gapped Time Anchors and Staleness Budgets
Audience: operators of sealed estates and engineers consuming posture.
Verified against commit
942a18727c(2026-09-26):src/__Libraries/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/Posture/,src/OfflineKit/__Libraries/StellaOps.AirGap.Time.Verification/,src/Cli/StellaOps.Cli/Commands/CommandHandlers.AirGap.TimeAnchor.cs.
A sealed estate cannot ask an external clock what time it is. Stella Ops instead records a trusted time anchor, an RFC3161 timestamp token verified against a timestamp-authority trust chain, and measures the age of that anchor against staleness budgets.
1. Where the anchor lives
The anchor is an environment-state document of class time-anchor held by Platform (PUT/GET /api/v1/platform/environment-state/time-anchor/{scope}). The document (AirGapTimeAnchorDocument, schemaVersion 1, format rfc3161) carries anchorTime, source, signatureFingerprint, tokenDigest, the token itself as proof, verifiedAt, revocationChecked and a stalenessBudget. A seal document (airgap-seal) can also carry an embedded timeAnchor together with a default budget and per-content contentBudgets.
Writing a time-anchor document requires envstate:time-anchor:write; there is no legacy alias.
2. Declaring and inspecting the anchor
# Verify the token locally, then declare it (default scope: installation)
stella airgap time-anchor declare --token anchor.tsr --trust-roots tsa-chain.pem [--source <label>] [--dry-run] [--json]
# Read the declared anchor; with --trust-roots, re-verify the stored proof
stella airgap time-anchor status [--trust-roots tsa-chain.pem] [--output json]
Verification uses Rfc3161Verifier: the CMS signature, the signer chain against the supplied roots, and revocation evidence (OCSP responses or CRLs) where provided. The TimeTokenFormat enum still names Roughtime, but no Roughtime verifier exists at this commit.
Consumers never trust the declared fields alone. AirGapTimeAnchorProofVerifier re-verifies the stored proof against the public trust bundle registered for the purpose timestamp-authority (Crypto:PublicTrust:CertificateBundles:timestamp-authority). On offlinekit-web that bundle is the PEM chain mounted at /app/etc/offlinekit/time-anchor-trust-roots.pem and bound through appsettings.Production.json.
3. Staleness evaluation
AirGapPostureReading.EvaluateStaleness(now, contentClass):
| Verdict | Condition |
|---|---|
Unknown | posture not declared or unreadable, or no anchor declared; deliberately not Fresh |
Fresh | anchor age below warningSeconds, or the anchor is future-dated |
Warning | age at or above warningSeconds and below breachSeconds |
Breach | age at or above breachSeconds |
The budget is the contentBudgets[contentClass] entry when present, otherwise the document’s default stalenessBudget (3600 s warning, 7200 s breach when not declared).
4. Where staleness is enforced
- OfflineKit import (
OfflineKitImportPostureGate, content classoffline-kit, scopeinstallation): on a sealed estate, an import is refused with409and one ofSEALED_IMPORT_TIME_ANCHOR_MISSING,SEALED_IMPORT_TIME_ANCHOR_UNREADABLEorSEALED_IMPORT_TIME_ANCHOR_BREACH; aWarningverdict admits with a logged warning. Unsealed or undeclared posture admits, and an unreadable posture replica admits with a logged warning. - Policy Engine serves
/system/airgap/staleness/{status,fallback,evaluate,recover}(src/Policy/StellaOps.Policy.Engine/Endpoints/StalenessEndpoints.cs).
5. Operator notes
- Refresh the anchor before it reaches its warning budget; a breached anchor stops sealed imports.
- Keep the timestamp-authority chain used for
declareand the chain mounted onofflinekit-webthe same, or the re-verification in §2 fails. - The predecessor
/api/v1/time/{status,anchor}endpoints and theAirGap:TrustRootFileconfiguration belonged to the retiredairgap-timehost; they have no successor. Archived:docs-archive/modules/airgap/guides/time-api.md.
