# Why does an instance fail AAIF credential binding when its runtime identity is re-derived after a token exchange **Date of Record:** 2026 November 13 **Scope:** Applies to AAIF-conformant agents that present delegated credentials (subject token → actor token exchanges) and whose runtime identity is computed from mutable instance attributes (pod UID, hostname, ephemeral workload ID). Does not apply to long-lived service identities pinned to a static keypair, nor to pure bearer-token flows with no actor-claim binding. ## Core Distinction AAIF credential binding is not a signature check on the credential itself; it is a *continuity* check between the identity asserted inside the credential and the identity the runtime currently presents to the verifier. Two distinct notions get conflated: 1. **Issuance binding** — at token mint time, the issuer records `(sub, act, instance_fingerprint, audience, exp)` into the binding record. 2. **Presentation binding** — at each use, the resource server re-derives the caller's fingerprint and compares it to the recorded one. The boundary failure occurs when the fingerprint is *re-derivable but not stable*: the runtime recomputes it from attributes that are legitimately allowed to change (pod rescheduling, autoscaler replacement, hostname reassignment) while the credential remains valid. The credential is cryptographically sound, unexpired, and correctly scoped — yet presentation binding fails, and the failure mode is indistinguishable, at the verifier, from token theft. ## What Was Observed In a multi-tenant agent mesh, delegated actor tokens were minted with an `instance_fingerprint` derived from `sha256(pod_uid + hostname + boot_nonce)`. A horizontal autoscaler replaced worker pods under load. The replaced pod's token, still within its TTL, was presented by the *successor* pod because the agent's session state (including the token) was restored from a shared checkpoint. The successor computed a new fingerprint. The verifier rejected with `binding_mismatch`, logged it as a suspected replay, and triggered a tenant-wide credential revocation — cascading to unrelated agents sharing the audience. Key observations: - The mismatch was deterministic, not intermittent: every resumed session failed on first presentation. - The failure surfaced only under scaling events, so staging (fixed replica count) never reproduced it. - Revocation policy treated `binding_mismatch` as adversarial, escalating a benign continuity gap into an availability incident. - Re-minting the token from the successor pod succeeded immediately, proving the credential was never compromised — only *discontinuous*. ## Condition of Validity The binding check is valid only when the fingerprint is **invariant across the credential's lifetime**. Formally, for a credential with validity interval `[t0, t1]`, the runtime must guarantee: ``` fingerprint(t) = fingerprint(t0) for all t in [t0, t1] ``` This holds for static identities and for fingerprints derived from immutable, credential-scoped attributes (e.g., a keypair hash, a hardware attestation quote, a long-lived SPIFFE ID). It does **not** hold when the fingerprint includes any attribute that the orchestration layer is permitted to mutate — pod UID, node name, IP, boot nonce, replica ordinal. In those cases the invariant is violated by design, and the binding check is measuring the wrong thing. ## Decision Boundary The correct decision is not "reject on mismatch" but "distinguish discontinuity from substitution": - If the presented credential's *issuance-binding record* is intact and the mismatch is explained by a **documented lifecycle transition** (reschedule, restore, migration) attested by the orchestrator, treat as **continuity gap** → require re-attestation and re-mint, do not revoke. - If the mismatch occurs with **no lifecycle transition** in the audit trail, or the fingerprint differs in a field that cannot change under any permitted transition (e.g., keypair hash), treat as **substitution** → revoke and escalate. The boundary is therefore: *is the changed field within the set of attributes the orchestrator may legitimately mutate during the credential's lifetime?* If yes, the fingerprint must not have included it; if no, the mismatch is adversarial. ## Operational Effect Agents that bind on mutable attributes experience availability failures precisely when the system is under the load that triggers scaling — the worst possible correlation. The remediation is to move the fingerprint to credential-scoped immutable material (keypair or attestation hash) and to demote mutable attributes to *advisory* metadata carried alongside the credential, checked only for anomaly scoring, never for binding. Verifiers must additionally classify `binding_mismatch` against the orchestrator's transition log before applying revocation, converting a false-positive replay alarm into a re-attestation handshake. ```python import hashlib from dataclasses import dataclass from typing import Optional class BindingDiscontinuityError(RuntimeError): """Raised when a credential's presentation fingerprint diverges from its issuance fingerprint without an attested lifecycle transition.""" pass @dataclass(frozen=True) class BindingRecord: subject: str audience: str issuance_fingerprint: str expires_at: float # Attributes the orchestrator is permitted to mutate during a credential's # lifetime. Including any of these in the binding fingerprint violates the # continuity invariant. MUTABLE_ATTRS = frozenset({"pod_uid", "hostname", "boot_nonce", "node_name", "ip"}) def compute_fingerprint(attrs: dict) -> str: # Only credential-scoped immutable material may enter the binding hash. immutable = {k: v for k, v in attrs.items() if k not in MUTABLE_ATTRS} if not immutable: raise BindingDiscontinuityError( "fingerprint derived solely from mutable attributes; " "continuity invariant cannot hold" ) material = "|".join(f"{k}={immutable[k]}" for k in sorted(immutable)) return hashlib.sha256(material.encode()).hexdigest() def verify_presentation( record: BindingRecord, presented_fingerprint: str, now: float, lifecycle_transition: Optional[str] = None, ) -> None: if now > record.expires_at: raise BindingDiscontinuityError("credential expired") if presented_fingerprint == record.issuance_fingerprint: return # Mismatch: distinguish continuity gap from substitution. if lifecycle_transition in {"reschedule", "restore", "migration"}: # Continuity gap: benign, require re-attestation and re-mint. raise BindingDiscontinuityError( f"continuity gap during '{lifecycle_transition}'; " "re-attest and re-mint, do not revoke" ) # No attested transition: treat as substitution. raise BindingDiscontinuityError( "binding mismatch with no lifecycle transition: " "possible credential substitution; revoke and escalate" ) ```