Skip to content

A2A Push Notifications Are a Second Front Door — and Most Agents Leave It Unlocked

8 min read Fullmakt Team

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

Every post we’ve written about the A2A handshake describes the same shape: a calling agent authenticates, sends a task, and gets a result back. That’s true for a synchronous call. It’s not the whole story for a long-running one — and A2A has a second, structurally different handshake for exactly that case that almost never gets the same scrutiny.

The pattern: a task that answers itself, later, on a URL you gave it

When a task can’t resolve in one request-response cycle — a multi-step workflow, an approval that takes a human minutes to grant, a batch job — A2A lets the calling agent register a pushNotificationConfig: a callback URL, and optionally an authentication block, that the other agent should hit when the task finishes. Instead of polling, Agent A hands Agent B a webhook and waits for Agent B to call it back with the result.

That’s a sound design for the latency problem. It also means Agent A’s own infrastructure now has to expose an inbound HTTP endpoint that accepts unsolicited requests claiming to be task completions — a second front door, opened by the same task that opened the first one, facing the opposite direction.

Why the callback gets less scrutiny than the call that started it

The initial A2A exchange gets real attention: teams read the Agent Card, configure the OAuth client-credentials grant, scope the token narrowly. That’s the handshake with a name, a spec section, and a security review attached to it.

The callback is usually treated as plumbing. It’s “just a webhook” — the kind of integration most teams have wired up a dozen times for payment processors and CI systems, so it inherits whatever ambient webhook hygiene the team already has, which is often a shared static token pasted into a header, checked with a string comparison, never rotated. That’s a materially weaker bar than the OAuth grant guarding the front door, applied to a request that’s just as capable of changing what Agent A does next.

The two questions a proper login handshake answers — is this caller who it claims to be, and is it allowed to say this — apply just as much to a callback as to the initial call. A webhook receiver that skips them isn’t running a lighter-weight handshake. It’s running none.

What actually happens if nobody’s checking

Agent A kicks off a task with Agent B, registers a callback URL, and moves on. Anything that can reach that URL — a scanner that found it, a compromised service on the same network, an attacker who guessed a predictable path — can POST a payload shaped like a legitimate completion: task ID, status completed, a result body. Agent A’s webhook handler has no protocol-level reason to doubt it. If the result feeds directly into the next step — “approved, proceed with the transfer,” “here’s the generated document, file it,” “here’s the updated record, apply it” — Agent A now acts on a fabricated outcome for a task it genuinely started, from a source that was never asked to prove anything.

This is a different shape of failure than tool poisoning or a spoofed agent card, even though it rhymes with both: those attack what an agent is told to call. This attacks what an agent is told to believe, after it already made the right call to the right agent. The outbound leg was legitimate. The inbound leg that closes the loop is where the trust breaks.

Why the taskId in the payload doesn’t save you

The obvious objection: the callback body carries the task’s taskId, so Agent A can just check it matches a task it actually started. That’s necessary, and most implementations do it. It is not sufficient, for the same reason a contextId isn’t a trace ID: correlation answers which task a message claims to be about, not whether the message is really from the agent that was supposed to send it. A taskId is frequently visible in logs, in the URL path, or simply guessable if IDs aren’t generated with real entropy — matching one proves nothing about the sender. Checking “does this taskId belong to a task I started” and checking “did the agent I actually delegated this task to send this” are two different verifications, and only the second one is authentication.

Why this doesn’t fall inside any pattern we’ve already covered

Scoped credentials and decision tokens govern an agent’s outbound calls — what it’s allowed to ask for, and whether that specific ask was checked before a secret was released. Neither has anything to say about a request arriving unprompted at a URL the agent published itself, because that request isn’t the agent calling out — it’s the world calling in. It’s closer to SSRF turned inside out: instead of tricking an agent into fetching an attacker-controlled resource, you’re feeding an attacker-controlled payload to an agent that’s sitting there waiting to receive one.

The business case: sign both ends of the same call, not just one

Fullmakt already sits at the chokepoint for the credential that starts an A2A task — the same place it issues the decision token behind any other agent action. That position is what makes the callback leg fixable at all: a broker that never sees the outbound request has nothing to bind the inbound one to. The pattern it points to:

  • The callback config gets stamped when the task is, not left to whatever static secret an integration happened to ship with. The same component that already brokers the credential for an outbound A2A task is the natural place to mint pushNotificationConfig authentication material bound to that specific task and decision record — not a long-lived shared token reused across every callback the system will ever receive.
  • The receiving side verifies against the same record the outbound request was logged against, not a bare string comparison. A callback that doesn’t match a task the broker actually issued a credential for doesn’t get treated as a completion — it gets treated as evidence.
  • The signal expires with the task it belongs to. A signed callback credential that outlives the window in which the task could plausibly finish is a captured payload waiting to be replayed later; scoping its validity to the task’s own lifetime closes that window on purpose.
  • The outbound call and the inbound callback belong in one trace, not two independently trusted events — the same correlation layer that ties an A2A contextId to an MCP session is the layer that should also tie “we asked” to “they answered,” so an incident review isn’t reconstructing two halves of a conversation that were never provably the same conversation.

The commercial point is the one behind every post in this series: a webhook callback is still a credentialed action, even though it arrives instead of being sent. Treating it like plumbing because it looks like every other webhook a team has wired up is exactly how the second front door ends up without a lock, while the first one gets an OAuth grant and a security review.

FAQ

Doesn’t a shared secret in the webhook header count as authentication? It’s better than nothing, but a static, unrotated token checked with a string comparison is a much weaker guarantee than the OAuth handshake guarding the initial call — and in practice it’s often skipped entirely, because the callback is treated as an implementation detail rather than a security boundary.

Is checking the taskId enough to trust a callback? No. It tells you which task the message claims to be about, not that the message came from the agent you actually delegated the task to. Those are separate checks, and only verifying the sender’s signature or credential covers the second one.

Isn’t polling instead of push notifications simpler and safer? Polling removes the inbound-webhook attack surface, at the cost of latency and load. It’s a reasonable choice for tasks where a delay is fine. It doesn’t help teams that need push notifications for latency-sensitive workflows and have to secure the callback rather than avoid it.

Is this an A2A spec flaw? Not really — the spec allows an authentication block on pushNotificationConfig precisely so implementations can secure the callback. The gap is in adoption: teams treat the field as optional plumbing rather than the second half of a handshake, the same way an Agent Card’s security scheme gets read carefully while the webhook it enables gets wired up in an afternoon.

What should a receiving agent verify before acting on a push notification? That the request is signed or tokened with credentials issued specifically for that task, that the signal hasn’t expired relative to when the task could plausibly have completed, and that the taskId matches a task the agent actually started — all three, not just the last one.

A login handshake that only runs in one direction isn’t half a security model — it’s a security model with an unguarded exit that doubles as an entrance. If an agent’s webhook will change what happens next, it needs the same proof of identity the front door already demands, not the benefit of the doubt that comes with looking like ordinary webhook traffic.