n4nAI

A2A protocol security: authentication between agents

A practical guide to a2a protocol security: implement authentication between agents using signed tokens, mTLS, OAuth2, and scoped API credentials.

n4n Team5 min read1,041 words

Audio narration

Coming soon — every post will get a voice note here.

Securing agent-to-agent communication starts with strict authentication at the protocol boundary. This guide lays out an actionable path for a2a protocol security that you can implement this week, covering credential types, token exchange, and transport hardening.

1. Map the trust boundary before issuing credentials

Agent meshes fail insecurely when teams treat every internal service as trusted. Draw the line: which agents are first-party, which are delegated by external systems, and which are ephemeral worker agents spawned per task. A planner agent that triggers a code-execution agent has a different risk profile than a logging sidecar.

A concrete registry helps. Store agent identities and their allowed peers so verification has a source of truth:

{
  "agents": {
    "planner-01": {
      "public_key": "ed25519:ab12...",
      "allowed_peers": ["retriever-07", "executor-03"],
      "ttl_hours": 24
    },
    "retriever-07": {
      "public_key": "ed25519:cd34...",
      "allowed_peers": ["planner-01"],
      "ttl_hours": 24
    }
  }
}

Without this map, a2a protocol security becomes a pile of hardcoded tokens that rot. Keep the registry in a signed config or a KMS-backed store, not in a plaintext repo. If you spin up agents dynamically (e.g., one per conversation), include a provisioning step that writes the new identity into the registry with a short TTL and strict allowed_peers.

2. Pick the credential primitive

Three options dominate: static API keys, shared HMAC secrets, and asymmetric signed assertions. Static keys are easy but leak-prone and cannot be scoped per caller. HMAC works for a small fixed set of agents but forces every party to hold the same secret, breaking least-privilege and making rotation a coordinated dance.

Use asymmetric keys. Each agent holds a private key; peers verify with the public key from the registry. This gives non-repudiation and lets you rotate without coordination downtime. Ed25519 is the right default: small keys, fast verification, no nonce management.

Generate an Ed25519 key pair:

openssl genpkey -algorithm ed25519 -out agent.key
openssl pkey -in agent.key -pubout -out agent.pub

The private key never leaves the agent process. The public key goes into the registry. If you operate across languages, JWK export is straightforward:

from cryptography.hazmat.primitives import serialization
import json
pub = serialization.load_pem_public_key(open("agent.pub","rb").read())
nums = pub.public_numbers()
jwk = {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": base64.urlsafe_b64encode(nums.x.to_bytes(32,"big")).decode().rstrip("=")
}

3. Implement a token issuance service

Agents should not sign arbitrary requests with their long-term identity key. Instead, run a lightweight token service (or use the agent’s own bootstrap) that mints short-lived JWTs with constrained claims. The token proves the agent acted, but its short life limits blast radius.

import jwt
import time
from cryptography.hazmat.primitives import serialization

private_key = serialization.load_pem_private_key(
    open("agent.key","rb").read(), password=None)

def mint_token(sub: str, aud: str, scope: list, ttl=300):
    now = int(time.time())
    payload = {
        "sub": sub,
        "aud": aud,
        "scope": scope,
        "iat": now,
        "exp": now + ttl,
        "jti": f"{sub}-{now}-{random.randrange(1<<16)}"
    }
    return jwt.encode(payload, private_key, algorithm="EdDSA")

The aud claim must name the specific target agent. A token minted for retriever-07 is useless at executor-03. That single field prevents lateral movement when one agent is compromised. Keep TTL at 300 seconds or less for chatty agents; batch jobs can use longer.

4. Verify tokens at every receiving agent

Verification is not optional, and it must happen before any business logic. In a FastAPI agent, write a dependency:

from fastapi import Depends, HTTPException, Request
import jwt

public_keys = load_registry()  # map agent_id -> pubkey

def require_auth(request: Request):
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(401, "missing bearer")
    token = auth[7:]
    unverified = jwt.decode(token, options={"verify_signature": False})
    aud = unverified.get("aud")
    if aud not in public_keys:
        raise HTTPException(403, "unknown audience")
    try:
        return jwt.decode(token, public_keys[aud],
                          algorithms=["EdDSA"], audience=aud, leeway=30)
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "expired")
    except jwt.InvalidTokenError:
        raise HTTPException(403, "invalid")

Note the two-step decode: we read aud to select the correct public key. If you skip this, you either trust one global key (bad) or do expensive trial verification. Cache the decoded claims per jti for the token lifetime to block replay.

5. Enforce transport security with mTLS

Application tokens prove who is calling. They do not prove the network link is private. Run mTLS between agents so a stolen token cannot be replayed from a rogue container outside the mesh.

With httpx:

import httpx

client = httpx.Client(
    cert=("agent.crt", "agent.key"),
    verify="mesh-ca.pem"
)
resp = client.post("https://retriever-07:8000/query",
                   json={"q": "status"},
                   headers={"Authorization": f"Bearer {tok}"})

The CA should be separate from your public web CA. Issue certs with short lifetimes (hours) via an internal PKI. Tradeoff: you now operate two credential systems. The payoff is that a2a protocol security survives a token leak because the TLS layer still rejects unknown clients. Watch out for cert expiry causing silent outages—automate renewal with a sidecar.

6. Scope permissions with claims, not endpoints

Do not encode authorization in URL paths alone. Add explicit scope claims listing permitted actions:

{
  "sub": "planner-01",
  "aud": "retriever-07",
  "scope": ["read:docs", "read:wiki"],
  "exp": 1710000000
}

The receiver checks scope against the requested operation. If a planner tries to call delete:index, reject at the edge. This keeps a2a protocol security granular without a central ACL service per request. Avoid overly broad scopes like "*"; if you need flexibility, use nested scopes (read:docs:prod vs read:docs:dev).

7. Common pitfalls

Clock skew. JWT exp validation breaks with loose clocks. Run NTP and tolerate a 30-second leeway via jwt.decode(..., leeway=30).

Key rotation stalls. If you never expire the registry public keys, a lost agent key is permanent risk. Use a dual-key period: publish new public key, keep old valid for 1 hour.

Replay within TTL. A token valid for 5 minutes can be replayed 100 times. Bind jti to a short nonce cache (Redis with EXPIRE), or shorten TTL to 60 seconds for chatty agents.

Logging tokens. Standard access logs often capture Authorization headers. Redact before emit, or use a separate signed header like X-Agent-Assertion.

Overwide audience. Minting a token with aud: "*" nullifies the containment built in step 3. Never do it.

Trusting inbound hostname only. mTLS protects the link, but if your agent verifies the cert chain but ignores the client CN, any valid mesh cert can call any agent. Bind the cert identity to the registry sub.

8. Tradeoffs and when to simplify

Full asymmetric a2a protocol security with mTLS and per-call JWTs is right for multi-tenant agent platforms or any system where agents are spawned by untrusted code. If you run a single-process agent loop on a locked-down host, a single HMAC secret with rotating daily values may be enough. You lose non-repudiation and fine-grained audience scoping, but you ship faster.

The cost of the full model is operational: you maintain a registry, a PKI, and token verification in every agent. The benefit is that compromise of one agent does not equal compromise of the mesh. Choose based on blast radius, not on hype. For mid-size deployments, start with signed tokens and skip mTLS until you expose agents outside the private subnet.

9. Minimum viable rollout order

  1. Write the agent registry JSON and load it at boot.
  2. Generate Ed25519 keys for each agent; store private keys in env or secret mount.
  3. Add the mint_token function to your bootstrap; emit tokens with aud, scope, and exp.
  4. Add the require_auth dependency to all agent HTTP routes.
  5. Stand up a mesh CA and enable mTLS client certs for cross-host traffic.
  6. Enforce scope claims in handlers before executing actions.
  7. Wire key rotation and jti replay cache.

Follow that order and you get defensible a2a protocol security without a rewrite. The patterns above are standard authN/Z lifted to the agent layer; the only novelty is the pace at which agents proliferate, which makes automation of credential life cycle non-negotiable. Build the registry first, verify strictly, and keep tokens short—everything else is tuning.

Tagsa2asecurityauthenticationagent-protocols

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agent-to-agent (a2a) communication protocols posts →