Skip to content

fix: use mTLS transport for outbound A2A calls to Google endpoints#6370

Open
ShresthSamyak wants to merge 5 commits into
google:mainfrom
ShresthSamyak:fix/a2a-agent-identity-mtls
Open

fix: use mTLS transport for outbound A2A calls to Google endpoints#6370
ShresthSamyak wants to merge 5 commits into
google:mainfrom
ShresthSamyak:fix/a2a-agent-identity-mtls

Conversation

@ShresthSamyak

Copy link
Copy Markdown

Summary

A2A remote calls made via AgentRegistry.get_remote_a2a_agent(...) fail with
401 UNAUTHENTICATED when the orchestrator uses Agent Identity, while
working fine with a standard Service Account. This routes outbound A2A traffic
to Google endpoints over a mutual-TLS channel so channel-bound credentials are
accepted.

Fixes #6365.

Root cause

get_remote_a2a_agent() builds a RemoteA2aAgent with only an optional
httpx_client, and RemoteA2aAgent creates a plain httpx.AsyncClient for
the actual A2A RPCs. Google context-aware access policies (used by Agent
Identity) issue access tokens that are cryptographically bound to a mutual-TLS
channel; presenting a bound token over a plain connection is rejected with
401 UNAUTHENTICATED.

The mTLS work in 03671c6 hardened only the AgentRegistry control-plane calls
(_make_request via AuthorizedSession). The A2A data-plane client never
carried the client certificate or a channel-bound token, so:

Identity Token Over plain client Result
Service Account unbound bearer accepted anywhere works
Agent Identity mTLS/DPoP channel-bound binding check fails 401

get_mcp_toolset() already injects Google auth for Google MCP endpoints;
get_remote_a2a_agent() had no equivalent for A2A. This aligns them.

Changes

  • New utils/_async_mtls_transport.py — the async google-auth mTLS
    transport (_RefreshableAsyncCredentials, _GoogleAuthAsyncTransport,
    _SharedAsyncTransport, _GoogleAuthAsyncByteStream) extracted from
    mcp_session_manager.py, plus a create_google_auth_mtls_transport(url)
    factory. Shared by both the MCP and A2A paths.
  • mcp_session_manager.py — imports the transport classes from the shared
    module instead of defining them locally. No behavior change.
  • remote_a2a_agent.py — new enable_google_auth_mtls flag; when set and
    no httpx_client is supplied, the default client is built on the mTLS
    transport, falling back to a plain client if mTLS cannot be negotiated.
  • agent_registry.py — enables the flag for Google A2A endpoints when a
    client certificate is configured and GOOGLE_API_USE_MTLS_ENDPOINT != "never".
    A caller-supplied httpx_client is never overridden; if it would suppress
    required mTLS, a warning is logged so the 401 is diagnosable.

Behavior / compatibility

  • Non-Google endpoints and Service Account flows are unaffected.
  • A user-provided httpx_client is always respected (with a warning when it
    would suppress required mTLS).
  • The mTLS path requires google.auth.aio (google-auth[aiohttp]); if
    unavailable, it degrades gracefully to a plain client.
  • No public API removed; enable_google_auth_mtls is an additive,
    default-False parameter.

Testing plan

Unit tests (pytest), all passing (230 passed):

  • tests/unittests/utils/test_async_mtls_transport.py — factory success /
    non-mTLS / exception / no-aio paths, and token host-scoping.
  • tests/unittests/integrations/agent_registry/test_agent_registry.py — mTLS
    enabled for Google endpoints; not enabled for non-Google, without a client
    cert, with a caller-supplied client, or when the endpoint is set to never;
    and that the suppressed-mTLS warning fires (and stays silent for non-Google).
  • tests/unittests/agents/test_remote_a2a_agent.py — default client is plain
    when disabled, uses the mTLS transport when enabled, and falls back when mTLS
    is unavailable.
  • tests/unittests/tools/mcp_tool/test_mcp_session_manager.py — unchanged MCP
    mTLS behavior after the extraction.

Additionally verified locally that create_google_auth_mtls_transport()
negotiates a real mutual-TLS channel and presents the client certificate to a
server that requires it, with the bearer token attached over that channel
(returns 200; a cert-less client is rejected at the handshake).

Live validation against a deployed Agent Runtime + Agent Identity setup is
pending; the success path emits
Successfully configured mTLS using AsyncAuthorizedSession at INFO.

AgentRegistry.get_remote_a2a_agent() handed RemoteA2aAgent a plain
httpx.AsyncClient for the outbound A2A RPCs. Google context-aware access
policies (Agent Identity) bind access tokens to a mutual-TLS channel, so
presenting such a token over a plain connection is rejected with a 401
UNAUTHENTICATED. The mTLS work in 03671c6 only covered the AgentRegistry
control-plane calls; the A2A data plane still used an unauthenticated,
non-mTLS client. Standard Service Account bearer tokens are unbound and so
worked either way, which is why the failure only surfaced with Agent Identity.

Extract the async google-auth mTLS transport out of the MCP session manager
into a shared utility (utils/_async_mtls_transport.py) and reuse it on the A2A
path. RemoteA2aAgent gains an enable_google_auth_mtls flag; when set and no
httpx_client is supplied, it builds its default client on a transport that
presents the client certificate and attaches a channel-bound token. The flag
is enabled automatically by get_remote_a2a_agent() for Google endpoints when a
client certificate is configured and GOOGLE_API_USE_MTLS_ENDPOINT is not
"never".

A caller-supplied httpx_client is never overridden. When such a client would
suppress required mTLS for a Google endpoint, a warning is logged so the 401 is
diagnosable instead of silent. When mTLS cannot be negotiated (no client cert
or google.auth.aio unavailable), it falls back to a plain client.

Fixes google#6365
@rohityan rohityan self-assigned this Jul 10, 2026
@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Jul 10, 2026
@rohityan rohityan added a2a [Component] This issue is related a2a support inside ADK. and removed core [Component] This issue is related to the core interface and implementation labels Jul 10, 2026
- _async_mtls_transport.py: mark the google.auth.aio import fallback shim
  with type: ignore and add __all__ so mypy treats the re-exported
  AsyncAuthorizedSession as an explicit export (fixes the attr-defined error
  in mcp_session_manager and the no-redef/misc errors on the moved shim).
- test files: use example.com instead of googleapis.com URLs so the mTLS
  compliance check is not tripped by incidental host strings in tests.
@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Jul 11, 2026
Satisfies the end-of-file-fixer pre-commit hook (the file ended with a
double newline).
@ShresthSamyak

Copy link
Copy Markdown
Author

Thanks for taking a look, @rohityan, and for merging main in.

I've pushed two follow-up commits to address the CI failures from this change:

  • e0157c9 — fixes the mypy and compliance checks. For mypy, I added __all__ to the new _async_mtls_transport module so AsyncAuthorizedSession is treated as an explicit re-export, and a # type: ignore on the google.auth.aio import fallback shim. For compliance, the new tests were using incidental googleapis.com hostnames, which I changed to example.com.
  • 3bebd5e — removes a stray trailing blank line in .agents/skills/adk-style/references/imports.md that was tripping the end-of-file-fixer hook.

A few of the remaining red checks appear unrelated to this PR's diff, but please let me know if I'm missing something:

  • Unit Tests: the failures are all test_verify_snippets.py erroring at collection with FileNotFoundError for open_source_workspace/.agents/skills/adk-verify-snippets/scripts/verify_md.py. That path looks specific to the internal checkout layout, so it may not resolve on the public runner — the rest of the suite passes.
  • Mypy: the one remaining flagged line is in models/lite_llm.py, which this PR doesn't modify.
  • Do Not Merge on GitHub / Copybara: the standard external-PR marker.

Happy to make any adjustments you'd like on the fix itself. Thanks again!

@rohityan rohityan removed the core [Component] This issue is related to the core interface and implementation label Jul 11, 2026
@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a2a [Component] This issue is related a2a support inside ADK. core [Component] This issue is related to the core interface and implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A fails using Agent Identity in Agent Runtime

3 participants