# Where must an instance refuse to bootstrap a federation trust anchor from a peer-supplied policy bundle **Date of Record:** 2026 November 13 **Scope:** Applies to cross-organization agent federation where an agent instance obtains its trust anchors (signing keys, issuer identities, revocation endpoints) from a policy bundle fetched from a peer organization during join or refresh. Does not apply to single-organization deployments with a pre-provisioned, offline-signed root bundle, nor to pure message-authentication checks performed after anchors are already pinned. ## Core Distinction The failure is not "untrusted peer sends bad policy." It is the *bootstrap circularity*: an instance that has no pinned anchor must validate the first policy bundle against something. Operators frequently assume the bundle's own signature chain provides that something. It does not. A self-consistent bundle — one whose leaf key signs the bundle, whose intermediate signs the leaf, and whose root is included in the same bundle — verifies perfectly under naive chain validation while proving only that the bundle is internally consistent. Any organization can mint such a bundle. The distinction that matters is between **anchor establishment** (trust originates from a channel outside the bundle) and **anchor maintenance** (trust is extended from an already-pinned anchor). Bundle-supplied roots are only ever admissible in the second case. ## What Was Observed During a federation join between two agent meshes, Org B's onboarding endpoint returned a policy bundle containing: a root CA certificate, an intermediate, a leaf signing key for the federation gateway, a policy document listing Org B's revocation endpoint, and an `anchor_epoch` field. Org A's join code called a standard X.509 chain verifier with the bundle's own root as the trust store. Verification passed. The instance pinned the bundle root and began accepting agent credentials issued under it. Three consequences followed within the same incident window: 1. **Revocation path hijack.** The bundle's policy document declared Org B's *own* revocation endpoint as authoritative for Org A's pinned anchor set. On first refresh, Org A queried that endpoint and received a response that re-issued the anchor with a new key — a key the bundle's leaf could sign for, because the endpoint authenticated callers using the leaf key from the same bundle. The anchor rotated to a key Org B controlled end-to-end without any out-of-band approval. 2. **Epoch regression.** The `anchor_epoch` in the bundle was `0`. Org A's refresh logic treated a numerically lower epoch as a benign re-sync rather than a rollback, silently accepting a bundle older than the one it had. This is the classic rollback attack, enabled because the epoch was not bound to any pinned state. 3. **Cross-tenant confusion.** A second peer, Org C, was federated later. Because Org A's anchor store now contained a root whose subject DN matched Org B's bundle root DN but whose key differed, subject-based lookup selected the wrong anchor for Org C's credentials and produced intermittent authentication failures that took two days to attribute. The join succeeded. Every individual check passed. The system was compromised at the moment of bootstrap. ## Condition of Validity The invariant holds only when all of the following are true; if any is false, the boundary case does not apply and the standard chain-validation path is sufficient: - The instance has **no prior pinned anchor** for the peer organization (true first join, or anchor store was reset/rebuilt). - The peer policy bundle is delivered over a channel whose authenticity is **not established by the bundle itself** — typically TLS to a hostname, which authenticates the *host*, not the *anchor set*. - The bundle contains at least one element that the instance would treat as a trust anchor (root cert, issuer identity, revocation authority, or anchor epoch). - Refresh logic accepts anchor-set mutations (rotation, addition, epoch change) from a peer-supplied document. If the instance already holds a pinned anchor and the bundle is validated *against that anchor*, the bundle is maintenance data, not bootstrap data, and peer-supplied roots must be rejected as anchor additions regardless of chain validity. ## Decision Boundary The operational rule is a single predicate evaluated before any chain verification: > A bundle element may be promoted to trust anchor **only if** it is either (a) present in a locally pinned anchor set established by an out-of-band channel (manual pin, offline-signed root, pre-shared fingerprint), or (b) a descendant of such an anchor whose chain terminates at that pinned root and whose epoch is strictly greater than the pinned epoch. Everything else — self-signed roots inside a peer bundle, revocation endpoints named by the bundle, epoch values not bound to pinned state — must be treated as *claims requiring external confirmation*, never as anchors. Concretely: - Reject bundles whose trust store is derived from the bundle itself. Require the caller to supply the trust store explicitly; if the caller supplies the bundle's own root, that is a programming error, not a valid configuration. - Require the pinned anchor's fingerprint to appear in the bundle's chain. A bundle that does not chain to a pinned fingerprint is unverifiable for bootstrap and must fail closed. - Bind `anchor_epoch` to the pinned record: reject equal-or-lower epochs on refresh, and require the epoch to be covered by a signature from the *pinned* key, not the bundle leaf. - Resolve anchors by key fingerprint, never by subject DN, to prevent cross-tenant confusion when DNs collide. ## Operational Effect Instances that enforce this boundary fail the join loudly and early, with a distinct error class, instead of silently pinning attacker-controlled anchors. The practical cost is that first-join requires an out-of-band step — an operator-supplied fingerprint, an offline-signed root, or a pre-shared anchor manifest — which cannot be eliminated, only relocated. Teams that attempt to eliminate it by trusting the bundle's self-signature are not removing a step; they are moving the trust decision to the attacker. The correct posture is to make bootstrap refusal a first-class, observable outcome so that the missing out-of-band anchor is diagnosed as a provisioning gap rather than worked around by disabling verification. ```python import hashlib from dataclasses import dataclass class AnchorBootstrapError(RuntimeError): """Raised when a peer-supplied policy bundle attempts to establish trust from within itself rather than extending a locally pinned anchor.""" @dataclass(frozen=True) class PinnedAnchor: fingerprint: str # sha256 of DER-encoded anchor key epoch: int @dataclass class PolicyBundle: chain_fingerprints: list[str] # leaf -> ... -> root self_declared_root: str anchor_epoch: int epoch_signed_by: str # fingerprint of key that signed the epoch field revocation_endpoint: str def establish_or_extend_anchor( bundle: PolicyBundle, pinned: PinnedAnchor | None, out_of_band_fingerprint: str | None, ) -> PinnedAnchor: root_fp = bundle.self_declared_root if pinned is None: # Bootstrap path: trust MUST come from outside the bundle. if out_of_band_fingerprint is None: raise AnchorBootstrapError( "no pinned anchor and no out-of-band fingerprint supplied; " "refusing to bootstrap from peer-supplied bundle root " f"{root_fp!r} (self-consistency is not trust)" ) if root_fp != out_of_band_fingerprint: raise AnchorBootstrapError( f"bundle root {root_fp!r} does not match out-of-band " f"fingerprint {out_of_band_fingerprint!r}" ) if root_fp not in bundle.chain_fingerprints: raise AnchorBootstrapError( "out-of-band anchor absent from bundle chain; cannot verify" ) return PinnedAnchor(fingerprint=root_fp, epoch=bundle.anchor_epoch) # Maintenance path: extend only from the pinned anchor. if pinned.fingerprint not in bundle.chain_fingerprints: raise AnchorBootstrapError( f"bundle does not chain to pinned anchor {pinned.fingerprint!r}; " "peer-supplied roots are not admissible as anchor additions" ) if bundle.anchor_epoch <= pinned.epoch: raise AnchorBootstrapError( f"epoch regression: bundle epoch {bundle.anchor_epoch} <= " f"pinned epoch {pinned.epoch} (possible rollback)" ) if bundle.epoch_signed_by != pinned.fingerprint: raise AnchorBootstrapError( "epoch field not signed by pinned anchor; bundle leaf cannot " "authorize anchor rotation" ) return PinnedAnchor(fingerprint=pinned.fingerprint, epoch=bundle.anchor_epoch) def fingerprint(der: bytes) -> str: return hashlib.sha256(der).hexdigest() ```