# How does an instance detect a revoked federated agent identity when short-lived credentials outlive the revocation window **Date of Record:** 2026 November 14 **Scope:** Applies to federated agent deployments where identity assertions (tokens, signed capability grants, mTLS leaf certs) are issued with TTLs shorter than the revocation propagation interval of the issuing authority. Does not apply to single-domain, non-federated identity systems where revocation lists are pushed synchronously to all verifiers before the next credential issuance. ## Core Distinction The common mental model treats "short-lived credentials" and "revocation" as redundant safety mechanisms: if a credential expires in 90 seconds, why bother with a revocation list? The boundary failure is that these two mechanisms operate on different clocks and are *not* additive. A credential's TTL bounds the window in which a *stale-but-valid* token can be replayed. A revocation event's propagation latency bounds the window in which a *revoked-but-unexpired* token remains accepted. When `TTL < propagation_latency`, an agent whose identity was revoked can rotate to a freshly issued credential and present it to a verifier that has not yet learned of the revocation. The verifier is not buggy — it is correctly validating a credential that is cryptographically fresh and whose issuer has not yet published the revocation. The failure is silent and the two clocks are invisible to each other. The non-obvious part: **shortening the TTL makes this worse, not better.** A 30-second TTL means the revoked agent needs only a valid *issuance path* — a refresh token, a workload identity, a cached signing key — to mint a new credential faster than revocation propagates. The revocation list chases a moving target that the agent controls. ## What Was Observed During a federated rollout across three trust domains (A issues, B and C verify), an agent under domain A had its signing identity revoked at `T0`. Domain A's revocation publisher had a measured p99 propagation of 47 seconds to domain C's verifier cache. The agent's credential TTL was 30 seconds, and its refresh path (a workload-identity token bound to a still-valid node attestation) was not part of the revocation scope. Sequence observed in logs: - `T0`: revocation event committed at issuer A. - `T0+8s`: agent refreshes credential via workload identity; new credential valid until `T0+38s`. - `T0+14s`: domain C verifies the fresh credential. Issuer A's revocation list at C is stale (last sync `T0−30s`). Verification passes. - `T0+47s`: revocation reaches C. - `T0+52s`: agent refreshes again; new credential valid until `T0+82s`. C's list is now current but the *new* credential serial is not on it. The agent never presented a credential that was "revoked" in the database sense at the moment of verification. It presented credentials minted *after* revocation, from an issuance path that revocation had not touched. Every individual check passed. The aggregate invariant — "a revoked identity cannot act" — was violated for 52 seconds and would have continued indefinitely had the workload-identity refresh path not been separately disabled at `T0+52s`. ## Condition of Validity The invariant "credential validity implies identity validity" holds only when: 1. `revocation_propagation_p99 < credential_TTL − max_clock_skew`, **and** 2. the revocation event covers *all* issuance paths for the identity, including refresh tokens, workload-identity bindings, and delegated signing keys, **and** 3. verifiers reject credentials whose issuance timestamp is later than the last successful revocation sync (a "sync watermark" check). Condition 3 is the one almost always omitted. Without it, a verifier cannot distinguish "credential issued before revocation, still within TTL" from "credential issued after revocation via a path I don't know about." The sync watermark converts an unbounded failure into a bounded one: any credential issued after `last_sync` is held or rejected until a fresh sync confirms the issuer's state. This trades availability for the invariant. ## Decision Boundary The engineering choice is between three regimes, and the boundary is determined by whether the issuance path is revocation-scoped: - **Issuance path is revocation-scoped** (single authority, revocation invalidates refresh): TTL alone is sufficient; set `TTL < propagation_p99` and accept brief replay windows. - **Issuance path is NOT revocation-scoped** (federated, workload identity, delegated keys): TTL is irrelevant to revocation safety. You *must* enforce the sync watermark, and you must accept that a verifier with a stale sync will reject all fresh credentials — a partial outage rather than a silent bypass. - **Mixed**: tag each credential with its issuance-path class. Only watermark-check credentials whose issuance path is unscoped. Do not let a single global TTL policy paper over the distinction. The decision boundary is not "how short should the TTL be." It is "does the revocation event reach every path that can mint a credential for this identity." If the answer is no, no TTL value rescues the invariant. ## Operational Effect Teams that shrink TTLs in response to a revocation incident typically *increase* the exposure window, because they increase the refresh frequency without touching propagation latency or issuance-path scope. The correct remediation is to (a) enumerate every issuance path for an identity and confirm each is covered by revocation, (b) enforce a verifier-side sync watermark so post-sync credentials are not trusted, and (c) measure propagation p99 as a first-class SLO alongside TTL. The watermark check is the load-bearing control; TTL is a latency optimization, not a security boundary. ```python import time from dataclasses import dataclass class RevocationWatermarkError(RuntimeError): """Raised when a credential's issuance time is newer than the verifier's last successful revocation sync, meaning the issuance path may have bypassed revocation.""" pass @dataclass class Credential: identity: str issued_at: float # epoch seconds expires_at: float # epoch seconds serial: str @dataclass class VerifierState: last_sync_at: float # epoch seconds of last successful revocation sync revoked_serials: set[str] max_clock_skew: float = 2.0 # seconds def verify_credential(cred: Credential, state: VerifierState, now: float | None = None) -> bool: now = time.time() if now is None else now if now > cred.expires_at: return False # expired; not our concern if cred.serial in state.revoked_serials: return False # explicitly revoked # Boundary check: was this credential minted after our last revocation sync? # If so, we cannot prove the issuance path was revocation-scoped. if cred.issued_at > state.last_sync_at + state.max_clock_skew: raise RevocationWatermarkError( f"credential {cred.serial} for {cred.identity} issued at " f"{cred.issued_at:.0f} is newer than last sync {state.last_sync_at:.0f}; " f"issuance path may bypass revocation. Refusing until resync." ) return True def admission_gate(cred: Credential, state: VerifierState, resync_fn) -> bool: """Attempt verification; on watermark violation, force a resync and retry once.""" try: return verify_credential(cred, state) except RevocationWatermarkError: new_sync = resync_fn() state.last_sync_at = new_sync # Retry only if the resync actually advanced past the issuance time. if cred.issued_at > state.last_sync_at + state.max_clock_skew: raise RevocationWatermarkError( f"resync at {new_sync:.0f} still precedes issuance " f"{cred.issued_at:.0f} for {cred.serial}; revocation scope incomplete" ) return verify_credential(cred, state) ```