# How should an instance decide when a federated trust anchor is stale rather than merely slow **Date of Record:** 2026 November 13 **Scope:** Applies to cross-organization multi-agent federations where peer instances establish identity via exchanged trust anchors (signed root documents, JWKS bundles, or equivalent) with a bounded refresh interval. Does not apply to intra-org deployments sharing a single trust root, nor to federations using synchronous online introspection on every call. ## Core Distinction The failure mode is not "the anchor is expired." It is the conflation of **anchor staleness** with **anchor unavailability**. A trust anchor that has passed its `nextUpdate` field is stale: its signatures may still verify, but the issuing authority has not re-asserted it within the promised window. An anchor that cannot be fetched is unavailable: the refresh channel failed. These require opposite responses. A stale anchor is a *validity* problem — the peer may have rotated keys, revoked a sub-anchor, or changed its federation scope, and continuing to trust the cached copy risks accepting a credential the authority has disowned. An unavailable anchor is a *liveness* problem — the cached copy is the best evidence available, and discarding it converts a transient network partition into a federation-wide outage. Most implementations collapse both into a single "refresh failed → distrust peer" path. That path is safe against key compromise and catastrophic against partition, because it makes every peer's availability a function of every other peer's reachability. ## What Was Observed During a three-org federation drill (Org A issuer, Orgs B and C relying parties), a BGP flap isolated Org C's anchor distribution endpoint for 11 minutes while its agent plane stayed reachable. Orgs A and B continued refreshing normally; C's peers did not. Within one refresh interval, B and C's mutual trust caches passed `nextUpdate` simultaneously. Both instances treated the miss as unavailability and evicted the peer anchor. When the route restored, neither side would re-fetch from the other because each now required a valid anchor to authorize the fetch — a mutual lockout that persisted for 40 minutes until an operator manually re-seeded anchors out of band. The non-obvious part: the anchors were *cryptographically fine* throughout. Signatures verified against the pinned federation root. The only signal was the `nextUpdate` timestamp, and the only correct reading of that signal was "stale, hold with degraded scope," not "untrusted, evict." ## Condition of Validity The distinction holds only when all of the following are true: 1. The anchor carries a signed `nextUpdate` and the federation root is pinned locally (so staleness is measured against a trusted clock, not a peer-supplied one). 2. A stale-but-cached anchor is stricter than a fresh one: it must be treated as **scope-reducing**, not scope-preserving. Concretely, a stale anchor may continue to authorize *renewal of already-established sessions* but must not authorize *new sub-anchor issuance, new key introduction, or scope expansion*. 3. The refresh channel failure is distinguishable from a *negative* response. A 404 or a signed revocation is authoritative unavailability of the anchor's validity — treat as untrusted. A timeout, 5xx, or TLS failure is channel unavailability — treat as stale. 4. Clock skew between the local instance and the federation root is bounded and monitored; otherwise `nextUpdate` comparison is meaningless. If any condition fails, fall back to conservative eviction — but log the eviction as a distinct event class so operators can tell staleness-driven lockouts from genuine revocations. ## Decision Boundary The boundary is a two-axis classification, not a single expiry check: - **Anchor signature valid against pinned root?** No → untrusted, evict, do not retry. - **Anchor within `nextUpdate`?** Yes → full scope. - **Anchor past `nextUpdate`, refresh channel reachable, refresh returned a signed document?** → adopt new anchor, full scope. - **Anchor past `nextUpdate`, refresh channel failed (timeout/5xx/TLS)?** → **stale-hold**: retain anchor, restrict to session renewal, enter backoff refresh, and emit a `StaleAnchorHeld` signal. Do **not** evict. - **Anchor past `nextUpdate` by more than the federation's `maxStaleWindow` (e.g., 2× refresh interval) with no successful refresh?** → escalate to untrusted; this is the point where staleness has outlived any plausible partition. The critical rule: eviction is triggered by *authoritative* signals (bad signature, signed revocation, exceeded `maxStaleWindow`), never by *channel* signals alone. ## Operational Effect Implementing stale-hold converted the drill's 40-minute manual lockout into a 90-second automatic recovery: Org C's route restored, backoff refresh succeeded on the next attempt, and both peers re-established full scope without operator intervention. The cost is a bounded window — `maxStaleWindow` — during which a peer operates on a possibly-revoked anchor. That window must be sized to the federation's measured partition duration distribution, not to the refresh interval, and it must be paired with the scope restriction so that a stale anchor cannot be used to introduce new keys. The invariant to encode and test: *no new key material or scope expansion is ever accepted on a stale anchor, and no anchor is evicted solely because its refresh channel failed.* ```python from dataclasses import dataclass from enum import Enum class FederationTrustError(RuntimeError): """Raised when an anchor decision cannot be made safely.""" class AnchorState(Enum): FULL = "full" STALE_HOLD = "stale_hold" UNTRUSTED = "untrusted" @dataclass class AnchorDecision: state: AnchorState allow_new_keys: bool allow_session_renewal: bool def classify_anchor( signature_valid: bool, now: float, next_update: float, refresh_outcome: str, # "ok" | "channel_failed" | "revoked" | "not_attempted" max_stale_window: float, ) -> AnchorDecision: if not signature_valid or refresh_outcome == "revoked": return AnchorDecision(AnchorState.UNTRUSTED, False, False) if now <= next_update: return AnchorDecision(AnchorState.FULL, True, True) if refresh_outcome == "ok": # Caller should have adopted the fresh document; reaching here is a bug. raise FederationTrustError( "fresh anchor reported ok but still past nextUpdate; " "clock skew or adoption failure must be resolved before trusting" ) if refresh_outcome == "channel_failed": if now - next_update <= max_stale_window: return AnchorDecision(AnchorState.STALE_HOLD, False, True) return AnchorDecision(AnchorState.UNTRUSTED, False, False) if refresh_outcome == "not_attempted": raise FederationTrustError( "anchor past nextUpdate with no refresh attempt; " "refusing to classify — schedule refresh before proceeding" ) raise FederationTrustError(f"unknown refresh outcome: {refresh_outcome!r}") ```