DEV Community

Cover image for A2A Core Defense: una capa de seguridad para el protocolo Agent2Agent (y por que el SDK oficial no basta)
Fenix
Fenix

Posted on

A2A Core Defense: una capa de seguridad para el protocolo Agent2Agent (y por que el SDK oficial no basta)

El gap que nadie había cerrado

El ecosistema Agent2Agent (A2A) está explotando en 2025-2026. Los agentes ya no se
hablan solo con un LLM: se coordinan entre sí, delegan tareas, disparan pagos (AP2 de
Google) y suscriben webhooks de notificación. Pero el SDK de referencia oficial,
a2aproject/a2a-python, tiene huecos de seguridad abiertos y confirmados por el propio
mantenedor
.

El más claro: issue #786 (OPEN).
El servidor pasa la URL de PushNotificationConfig directo a httpx.post() sin validar
nada. Un cliente malicioso puede registrar un webhook que apunte a la metadata cloud
interna (169.254.169.254), a 127.0.0.1 o a cualquier IP privada, y provocar SSRF.
El propio mantenedor reconoce que el default path is insecure.

No es el único frente. La investigación de 2025-2026 ya cuantificó las superficies:

  • arXiv:2608.23858Beyond the Mandate: Security Analysis of AP2 (Google).
  • arXiv:2608.24069Poisoning Agentic Alpha: vulnerabilidades adversariales en la comunicación inter-agentes de trading multi-agent.
  • arXiv:2608.02698 / 2604.01151 — colusión encubierta (stego) en poblaciones de agentes.

Nadie había empaquetado una defensa completa. Ese es el nicho.

Qué es A2A Core Defense

Un shield (inspector + validadores reutilizables) que se coloca delante o alrededor
de un servidor/cliente A2A. No es un fork del SDK oficial: es una capa que cada mensaje
atraviesa por un pipeline de 7 fases.

# Fase Qué hace
1 Parseo Valida el envelope contra el esquema oficial A2A.
2 Auth de conexión Exige un principal identificado (WARN en modo permisivo, DENY en estricto).
3 Capability token Auth por operación (Ed25519): sub / resource / actions / jti.
4 SSRF guard Bloquea file://, gopher://, loopback, metadata cloud, con pinning de IP + redirects off.
5 Agent Card Verifica la firma de la Agent Card (canonicalización RFC 8785, evita el bug #1174).
6 Colusión Detector black-box de coordinación encubierta en mensajes inter-agentes.
7 Forense Traza hash-encadenada para attribution.

Cómo se cierra de verdad el SSRF del issue #786

El vector es simple: PushNotificationConfig.url llega sin validar. A2A Core Defense lo
intercepta en la Fase 4 con predicados componibles:

from a2a_core_defense.phases.ssrf_guard import SSRFConfig, check_url
from a2a_core_defense.validators import URLValidationError

cfg = SSRFConfig()  # bloquea file://, gopher://, IPs privadas/loopback/metadata
try:
    check_url("https://169.254.169.254/latest/meta-data/", cfg)
except URLValidationError as e:
    print("bloqueado:", e)  # nunca llega a httpx.post
Enter fullscreen mode Exit fullscreen mode

Pero validar no basta: queda la ventana TOCTOU de DNS-rebinding (validas una IP, pero
el envío resuelve otra). El fix real fija la IP ya validada en la conexión y separa la
verificación TLS del host real usando request.extensions["sni_hostname"]:

# El server A2A debe disparar el webhook con esto, no con httpx.post crudo:
from a2a_core_defense.phases.ssrf_guard import send_push_notification
await send_push_notification(url, payload)  # IP pinneada + SNI=host real + redirects off
Enter fullscreen mode Exit fullscreen mode

Esto es crítico: si pones la IP en la URL de conexión, httpx verifica el certificado
contra la IP (que no está en el SAN) y cualquier webhook HTTPS real falla. El
sni_hostname hace que la conexión TCP vaya a la IP pinneada pero el handshake y la
verificación de certificado usen el hostname real.

La prueba de fuego (test local, sin red externa)

No me fio de un test que mockee el socket. Este test levanta un servidor HTTPS local con
un certificado autofirmado cuyo SAN es servidor-legitimo.test y demuestra ambos casos:

# Test A (positivo): IP 127.0.0.1 + sni=servidor-legitimo.test -> 200 OK
# Test B (negativo):  IP 127.0.0.1 + sni=otro-dominio.test  -> RECHAZADO
def test_f3_local_negative_wrong_sni(https_server):
    ca_path, port = https_server
    with pytest.raises((ssl.SSLCertVerificationError, httpx.ConnectError)):
        asyncio.run(_request(ca_path, port, "otro-dominio.test"))
Enter fullscreen mode Exit fullscreen mode

Salida cruda:

[F3 LOCAL-A] status=200 sni=servidor-legitimo.test -> OK (cert verificado contra host real)
[F3 LOCAL-B] sni=otro-dominio.test -> RECHAZADO (hostname no coincide con cert)
2 passed
Enter fullscreen mode Exit fullscreen mode

Si el Test B conectara con éxito, el pinning no verificaría hostname y el fix estaría
roto. Falla → el pinning es real.

Cómo usarlo

python -m venv .venv && . .venv/bin/activate
pip install -e .
Enter fullscreen mode Exit fullscreen mode
# Auditar un endpoint remoto (hace fetch real del Agent Card, protegido contra SSRF)
a2a-defend audit https://tu-agente.example.com/.well-known/agent.json

# Proxy inspector delante del server A2A (extrae el principal del request)
a2a-defend serve --listen 127.0.0.1:8080 --upstream https://tu-agente.example.com \
    --card-public-key pub.pem --strict

# Emitir un capability token de prueba
a2a-defend token --sub agent-a --resource 'a2a://server/*' --actions a2a.send,a2a.get
Enter fullscreen mode Exit fullscreen mode

Como biblioteca:

from a2a_core_defense import DefensePipeline, PipelineConfig

pipe = DefensePipeline(PipelineConfig(strict_card=True, card_public_key=pub_key))
verdict = pipe.inspect(raw_envelope, caller="agent-a", capability_token_raw=token_raw)
print(verdict.decision, verdict.reasons)
Enter fullscreen mode Exit fullscreen mode

Limitaciones (honestas)

No es magia. Las documento en el README porque importan:

  • El caller es identidad auto-declarada. El proxy lo extrae de un header (Authorization: Bearer / X-A2A-Caller) pero no verifica la firma del JWT. Si el proxy es el borde de red sin firma verificada aguas arriba, es falsificable. En modo no-estricto una operación sensible sin principal se marca WARN y pasa; con --strict se deniega.
  • Sin card_public_key, los capability tokens no se verifican por firma (degradación para dev). Pásala en producción.
  • La traza forense vive en memoria; no persiste entre reinicios.
  • El pinning solo aplica si disparas el webhook con send_push_notification(), no con httpx.post crudo.
  • No cubre RAG poisoning (es otro nicho).

Por qué existe este patrón

Encontré el nicho aplicando el mismo método que con MCP Core Defense: buscar dónde la
investigación ya cuantificó un problema pero la industria aún no había construido la
solución. El issue #786 es la confirmación de que el gap es real, no teórico.

Licencia y código

AGPL-3.0-or-later — Copyright (C) 2026 Pedro Sordo Martínez.

Repositorio: https://github.com/amurlaniakea/a2a-core-defense

40 tests, ruff limpio, verificado en clone limpio. Si mantienes un server A2A o estás
prototipando comercio agentico (AP2), esto te cierra los huecos más obvios antes de
producción.

Top comments (0)