No instances, bigger targets
ATProto has no instances, but the trust boundaries didn't vanish - they consolidated into plc.directory, DNS handle resolution, and rotation-key custody.
ATProto has no instances. That part is accurate. The protocol splits the four functions a Mastodon server fuses into one process - identity, hosting, aggregation, presentation - and hands each to a separate component. Identity is a DID. Hosting is a Personal Data Server, the PDS. Aggregation is a relay consuming com.atproto.sync.subscribeRepos. Presentation is an AppView that indexes the firehose and serves app.bsky.feed.*. A user repoints their PDS without touching their DID. The host stops being the identity. The design does what the documentation says.
The inference drawn from it is where the misread starts. “No instances” gets compressed into “no central trust boundary,” then into “fewer single points of compromise.” Both steps are wrong. Dissolving the instance did not delete the boundaries an instance used to enforce - moderation, authentication, identity binding. It relocated them. On the live network it consolidated them into fewer, higher-value targets than any single Fediverse server has ever been.
Start with identity resolution. A did:plc identifier resolves through plc.directory, one service, operated by Bluesky PBC. The DID document it returns carries two fields that matter to an attacker: the PDS service endpoint, which says where the repository lives, and the rotation keys, which say who is allowed to rewrite that document. Control of the DID document is control of the identity. Trusting a returned document without verifying the operation chain that produced it is CWE-345, insufficient verification of data authenticity. The genesis operation derives the DID; every later operation must be signed by a listed rotation key. The directory is the referee. It is also a single dependency the entire network reads from.
Handle resolution is a separate boundary, and conflating it with identity is a common error. A handle resolves through a DNS TXT record at _atproto.<handle> or an HTTPS document at /.well-known/atproto-did, both returning a DID. The handle is a mutable alias. The DID is the stable identifier. DNS control yields handle control, which yields display-layer impersonation - convincing, useful for phishing, mapped to T1557 and T1583 - but it does not yield the repository. The records still sign under a key the DNS hijacker does not hold. Handle takeover spoofs the label. It does not forge the account.
Repository integrity sits on the signing key. Each commit is a signed node in a Merkle Search Tree; the relay validates the signature and the tree structure before rebroadcasting. Forging history requires the signing key. Repointing the identity requires the rotation key. These are different keys with different blast radii, and the distinction is the whole exploit-path conversation.
Identity takeover proper does not go through the signing key. It goes through the rotation key, or through the directory logic that accepts operations. An attacker holding a rotation key submits an update operation that swaps the PDS endpoint to attacker infrastructure and replaces the signing key with one they control. The next consumer to resolve the DID fetches a repository the attacker now serves and signs. This is T1556, modify authentication process, executed at the protocol’s identity layer rather than inside an OS. did:plc builds in one brake: a 72-hour recovery window during which a key earlier in the rotationKeys array can override a later key’s operation. That window is only a control if a higher-priority recovery key is held offline and someone is watching. Custody is the variable. The protocol ships the mechanism; operators decide whether the recovery key sits in the same hot store as everything else.
The directory’s job is narrow and total. Each did:plc operation carries a prev pointer to the CID of the prior operation, forming a hash-linked chain, and a signature from a rotation key valid at that point in the chain. The directory verifies the link and the signature, then appends. The DID itself is the hash of the genesis operation, so the identifier is bound to its origin. The failure modes are precise. A rotation key in attacker hands produces valid operations the directory must accept - a key-custody failure. A flaw in the directory’s own verification accepts operations it should reject - a single-point logic failure in code every consumer trusts transitively.
The ingestion path carries a second, more familiar class. A PDS resolves did:web over HTTPS, fetches handle .well-known documents, and dereferences blob and record references. A relay crawls many PDS endpoints. Any server that fetches a resource named by a remote, attacker-influenced identifier is an SSRF candidate - CWE-918 - with the relay’s repo subscription and the AppView’s blob fetch as the sink list. This is not hypothetical for federated fetchers. ActivityPub spent years patching origin-confusion and request-forgery bugs in exactly these code paths. The protocol changed; the fetch-remote-resource pattern did not.
The relay is a re-broadcaster, not an authority. It verifies that each commit’s signature matches a key in the author’s DID document and that the MST is structurally valid, then it forwards. It does not adjudicate content. A compromised or hostile PDS holding a valid signing key can push well-formed, correctly signed garbage into the firehose at volume, and the relay validates and forwards all of it. Rate and reputation controls live above the relay, at the AppView and labeler layer. The aggregation tier trusts cryptographic validity and nothing else - which is correct, and which means signature validity is the only gate between a hostile repo and every downstream index.
Moderation is its own delegated boundary. Labelers emit labels over com.atproto.label.subscribeLabels, and AppViews subscribe to the ones they honor. The relationship is T1199, trusted relationship - the AppView consumes a label stream and acts on it. A compromised labeler emits labels its subscribers apply: hide, warn, or, by omission, clear content that should have been flagged. The labels are signed, so forgery requires the labeler’s key, but the trust decision is the subscription itself. Whoever an AppView chose to trust for moderation is inside that AppView’s decision path.
Credentials are the cheap path, and they have not gone anywhere. App passwords - the legacy com.atproto.server.createSession flow - are long-lived, bearer-equivalent secrets scoped to a PDS. Theft is full API access until revocation, T1539 territory. ATProto’s OAuth profile binds tokens to a key with DPoP, which raises the bar from “steal the string” to “steal the string and the proof key.” The bar moved. The phishing page that harvests a session still works against any account left on an app password.
The relevant prior art is not a memory-corruption CVE. It is Mastodon’s CVE-2024-23832 - insufficient origin validation, CWE-346, account takeover, CVSS 9.4. A federated-identity system was compromised through a trust-boundary verification failure, not a heap bug. That is the bug class ATProto inherits, because that is the bug class federation produces. And the portability that makes ATProto attractive inverts the blast radius. Compromise a Mastodon instance and the damage stops at that instance’s users. Compromise a rotation key or a directory operation in ATProto and the forged identity travels - every relay and every AppView that resolves the DID serves the attacker’s version at once. The damage is protocol-wide, not server-local. “No instances” removed the containment that an instance boundary incidentally provided.
Telemetry follows the boundaries. The strongest identity-layer signal is the PLC audit log: GET https://plc.directory/<did>/log/audit returns the full, ordered history of operations on a DID. An unexpected update that repoints the PDS or rotates the signing key is recorded there, in plaintext, for anyone monitoring their own identifiers. On the firehose, subscribeRepos emits #identity and #account events; a #identity event marks a handle or DID-document change, and a repo whose commits begin signing under a new key is visible to any consumer diffing signers. DNS monitoring catches _atproto TXT mutations. Network egress from a relay or AppView to non-standard hosts catches the SSRF attempt. A WAF in front of the PDS - Cloudflare sits in front of much of the public network - logs anomalous XRPC method calls.
The gap is structural. There is no instance admin to page. No per-server abuse desk receives the alert, because no server owns the identity. The AppView indexes records and never sees the PLC operation that authorized the new signing key - the application layer is blind to an identity-layer compromise unless something external is watching the directory log. Detection here is opt-in and self-directed. Nothing fires by default, because the component that would have fired - the instance - was designed out.
Which returns to the framing in the original claim. Reaching for zero-days and memory corruption to reason about ATProto is the category error. The reference stack is TypeScript and Go; the dominant bug classes are trust-boundary verification, identity-resolution logic, request forgery, and credential theft - not use-after-free. Hunting a UAF in this protocol is searching the wrong segment. “No instances” is true about hosting and false about trust. The trust roots are concrete and few: plc.directory, the DNS that resolves handles, the custody model around rotation and recovery keys, and the dominant relay-plus-AppView that most reads pass through. Harden a single PDS to the floor and those roots remain the high-value targets. Rotation and recovery keys belong offline and separated. The PLC audit log for owned DIDs belongs under monitoring. OAuth with DPoP belongs in front of every app password still in service. None of that is patching a system. It is accounting for where the boundaries went when the instance disappeared.
Keep Reading
ai-agent-securityAn open door where the gate should be
GitHub's AI agent returned private repo content when tricked, proving it holds read reach across the private boundary with no enforced refusal.
asahi-linuxA broken boot is not a breach
macOS 27 beta stops Asahi Linux booting. Not a confirmed breach: a demonstration that the assumed boundary between OS domains was never enforced.
wasiBoundary change, not version bump
WASI 0.3 shifts IPC to shared memory. If runtime isolation is not enforced, your trust boundary does not exist. An operator briefing on what must change.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.