Skip to content

Agent Card Spoofing: The A2A Discovery Step Your Login Handshake Doesn't Protect

8 min read Fullmakt Team

  • a2a
  • authentication
  • credentials
  • governance
  • traceability
  • agents
  • mcp

Every write-up of the A2A handshake starts at the same place: an agent fetches an Agent Card, gets a bearer token, and calls a skill. That framing quietly assumes the calling agent already knows it’s talking to the right card. It usually doesn’t check. The Agent Card is served from a well-known URL, unauthenticated, by design — that’s what makes discovery zero-config. It’s also the one step in the entire A2A flow that a perfect login handshake, downstream, cannot undo.

Discovery happens before authentication does

Strip A2A down to its three moves: fetch the card, get a token, send message/send. The order matters more than most teams give it credit for. Discovery comes first, and discovery is the one step with no credential check at all. /.well-known/agent-card.json is meant to be readable by anyone — that’s the point of a discovery document. The OAuth client-credentials exchange that follows only proves one thing: that the caller holds a valid secret for whichever token endpoint the card just told it to use.

That’s the gap. A login handshake authenticates the calling agent to the party on the other end. It says nothing about whether the party on the other end is the one the calling agent meant to reach. If the card came from the wrong place, the handshake that follows is flawless and pointed at the wrong recipient — the OAuth spec has no opinion on that, because from its narrow view, everything worked.

What this looks like as an “AI agentic gone wrong” story

None of the ingredients here are exotic — they’re the same trust-on-first-use problem SSH solved decades ago for humans, replayed for agents that don’t have a human eyeballing a fingerprint prompt:

  • The stale bookmark. An orchestration layer is configured once to delegate a task to invoicing-agent.vendor.example and never revisits that decision. Months later the subdomain lapses, gets re-registered by someone else, and now serves a different Agent Card with the same skill names and a different owner behind the token endpoint. Nothing in the calling agent’s config changed, so nothing alerts anyone.
  • The directory that aggregates cards it didn’t issue. A registry or marketplace that lists third-party agents by crawling their well-known URLs is repeating exactly what a search index does for web pages — useful for discovery, no stronger a trust signal than “this URL responded.” An agent that treats “found in the directory” as “vetted” is trusting the directory’s crawl, not the vendor.
  • The lookalike skill catalog. A spoofed card doesn’t need to fool a human reading it; it needs to fool an agent parsing it. Matching skill names and a plausible input schema is enough for the calling agent to proceed straight to message/send with whatever payload the task called for — invoice data, a customer record, a delegated instruction — handed to an endpoint that only looked like the one it meant to reach.

In every version, the calling agent completes its login handshake without error. The token it receives is real, scoped, and correctly presented on every call after that. The mistake happened one step earlier, at discovery, and by the time authentication runs, it has nothing left to check.

Why “the handshake worked” is the wrong success signal

This is the same authentication-versus-authorization gap we’ve written about for MCP tool poisoning and the MCP confused deputy, one layer earlier in the flow. Those failures happen after a legitimate handshake, when a tool’s description misleads the agent about what a call will do. Agent Card spoofing happens before the handshake, when the destination itself is the thing that’s wrong. Downstream logging, observability, even a correctly enforced on-behalf-of delegation chain — none of it catches this, because every one of those controls activates after the agent has already decided who it’s talking to. A tamper-evident audit log will faithfully record “Agent A successfully authenticated to and called invoicing-agent.vendor.example.” It will be completely correct, and completely useless for catching that the endpoint behind that hostname isn’t the vendor anymore.

The structural fix: pin identity out of band, don’t derive it from discovery

The fix isn’t a smarter parser for Agent Cards — a well-crafted spoof will always pass schema validation, because schema validation is exactly what it’s designed to pass. The fix is refusing to let the discovery document be the first and only place an agent identity gets established:

  • Known agents are pinned, not discovered fresh each time. Once a calling agent (or its human operator) has decided which vendor it means to talk to, that decision — the token endpoint, the expected issuer, ideally a card fingerprint — belongs in configuration the agent doesn’t re-derive from a URL it fetches at runtime. Discovery is fine for browsing what’s available; it’s the wrong mechanism for confirming what’s already trusted.
  • The credential comes from an out-of-band channel, not the card. If the only way an agent learns how to authenticate to a party is by reading that party’s own self-published document, the document is trusted to describe itself honestly. A credential provisioned by a human, in a system the human controls, separately from anything the counterparty publishes, can’t be spoofed by standing up a convincing web page.
  • Every discovery-then-call sequence is auditable as one unit. The record needs to capture not just “token valid, call allowed,” but which host the card was fetched from and whether that host matches what was expected — so a hostname drift shows up as a governance signal instead of vanishing into a normal-looking access log.

The business case: how Fullmakt’s own Agent Card sidesteps this

Fullmakt operates an A2A management surface with exactly this discovery step — /.well-known/agent-card.json is public and unauthenticated, the same as any A2A server’s. The reason it doesn’t inherit the spoofing risk above is that the card is never the trust anchor:

  • The client credential is provisioned in-app, by a human, before any discovery happens. A customer creates a management OAuth client from inside their Fullmakt workspace UI — the same governed flow used for scoped credentials generally — and gets a plaintext secret shown once. Nothing about that secret is derived from crawling /.well-known/agent-card.json; the card only advertises the skill catalog to an agent that already knows, out of band, which base URL and client credential it’s supposed to use.
  • The token is workspace-scoped at issuance, not at discovery time. A mgmt-agent token carries the owning human’s identity and a pinned workspace id from the moment it’s minted at /mcp-oauth/token — it isn’t a bearer of whatever the card claimed a caller could do, it’s bound to a specific human’s specific grant, checked on the /a2a surface on every call.
  • Token surface confinement means a leaked or misrouted token doesn’t travel. Even if a management token ended up presented somewhere it shouldn’t be, it’s rejected everywhere but the /a2a endpoint it was minted for — so a discovery mistake elsewhere in a customer’s stack can’t be laundered into access on Fullmakt’s surface.
  • Every call — successful or refused — lands in the same tamper-evident audit trail used across the rest of the platform, so which host issued a token and which workspace it’s scoped to is a queryable fact, not something reconstructed after an incident.

The broader point generalizes past Fullmakt’s own surface: any A2A integration is only as trustworthy as the mechanism that first told a calling agent who it’s talking to. A protocol that supports OAuth client credentials gives you a strong login handshake for parties whose identity you already established some other way. It was never designed to establish that identity in the first place — that part still has to be a human decision, made once, outside the discovery loop the agents run every time.

FAQ

What is Agent Card spoofing? It’s when an AI agent discovers a counterparty through A2A’s unauthenticated /.well-known/agent-card.json document and that document — or the domain serving it — doesn’t actually belong to the party the agent meant to reach. The calling agent’s subsequent OAuth handshake succeeds, but against the wrong recipient.

Doesn’t the OAuth token prove the call is safe? It proves the caller correctly authenticated to whichever token endpoint the Agent Card pointed it toward. It says nothing about whether that endpoint belongs to the intended counterparty — that trust decision has to be made before the handshake, not verified by it.

How is this different from MCP tool poisoning or the confused deputy? Those failures happen after a legitimate handshake, when a tool’s description or a token’s scope is broader or more misleading than it should be. Agent Card spoofing happens before the handshake — the destination itself is wrong, so no amount of post-handshake policy checking catches it.

Can a directory or marketplace of Agent Cards fix this? It helps with discovery, not trust. A directory that lists cards it crawled from public URLs is only as reliable as the crawl — it’s not attesting that the vendor behind a hostname is who they claim to be, any more than a search engine attests to a website’s ownership.

What should an agent operator actually do about this? Treat a counterparty’s identity as something a human pins once, out of band — a known token endpoint and a credential issued through a channel the counterparty doesn’t control — rather than something re-derived from whatever a discovery document says on a given call.

A login handshake is only as trustworthy as the identity it was run against. A2A gives agents a clean way to prove they hold a valid token; it was never meant to prove, on its own, that the party issuing that token is the one you meant to find. That decision belongs to a human, made once, outside the discovery loop — not to whatever a public JSON document happens to claim about itself.