diff --git a/docs/docs/documentation/components/proxies.md b/docs/docs/documentation/components/proxies.md index f53dcccef0..85de08971f 100644 --- a/docs/docs/documentation/components/proxies.md +++ b/docs/docs/documentation/components/proxies.md @@ -199,6 +199,36 @@ The proxy caches OAuth tokens and automatically refreshes them when they expire. Always use environment variables for sensitive credentials. Never commit tokens or secrets directly in configuration files. ::: +## SSL Certificate Verification + +By default, the proxy verifies SSL certificates when connecting to downstream agents over HTTPS. You can disable certificate verification for specific agents using the `ssl_verify` configuration option. + +### When to Disable SSL Verification + +Disable SSL verification (`ssl_verify: false`) only in these scenarios: + +- **Development/Testing**: Connecting to agents with self-signed certificates in non-production environments +- **Internal Infrastructure**: Agents running on internal networks with private CA certificates not trusted by the system + +:::warning[Security Warning] +Disabling SSL verification removes protection against man-in-the-middle attacks. Never disable SSL verification in production environments unless you fully understand the security implications. +::: + +### Configuration + +```yaml +proxied_agents: + - name: "production-agent" + url: "https://api.example.com/agent" + ssl_verify: true # Default - verify against system CAs + + - name: "dev-agent-self-signed" + url: "https://dev.internal.example.com/agent" + ssl_verify: false # Disable verification for self-signed certs +``` + +The `ssl_verify` setting applies to both agent card fetching and task invocations for the configured agent. + ## Custom HTTP Headers The proxy supports custom HTTP headers for both agent card fetching and A2A task invocations. This is useful for scenarios like API versioning, tenant identification, custom authentication schemes, or any other header-based requirements. diff --git a/examples/a2a_proxy_example.yaml b/examples/a2a_proxy_example.yaml index 0fe1d3fd40..e433754eca 100644 --- a/examples/a2a_proxy_example.yaml +++ b/examples/a2a_proxy_example.yaml @@ -60,3 +60,9 @@ apps: # request_timeout_seconds: 180 # # use_agent_card_url: false # If true (default), uses URL from agent card for tasks. # # If false, uses configured URL directly for all calls. + + # Example 3: Agent with self-signed certificate (development/testing) + # - name: "DevAgent" + # url: "https://dev.internal.example.com/agent" + # ssl_verify: false # Disable SSL verification for self-signed certs + # # WARNING: Only use in development/testing environments diff --git a/src/solace_agent_mesh/agent/proxies/a2a/component.py b/src/solace_agent_mesh/agent/proxies/a2a/component.py index cb71d3ba45..8b7e8c58f6 100644 --- a/src/solace_agent_mesh/agent/proxies/a2a/component.py +++ b/src/solace_agent_mesh/agent/proxies/a2a/component.py @@ -627,8 +627,16 @@ async def _fetch_agent_card( else: log.debug("%s Fetching agent card without authentication", log_identifier) + ssl_verify = agent_config.get("ssl_verify", True) + if not ssl_verify: + log.warning( + "%s SSL verification disabled for agent '%s'. " + "This should only be used in development environments.", + log_identifier, + agent_name, + ) log.info("%s Fetching agent card from %s", log_identifier, agent_url) - async with httpx.AsyncClient(headers=headers) as client: + async with httpx.AsyncClient(headers=headers, verify=ssl_verify) as client: resolver = A2ACardResolver(httpx_client=client, base_url=agent_url, agent_card_path=agent_card_path) agent_card = await resolver.get_agent_card() return agent_card @@ -1284,6 +1292,14 @@ async def _get_or_create_a2a_client( # Create a new httpx client with the specific timeout and custom headers for this agent # httpx.Timeout requires explicit values for connect, read, write, and pool + ssl_verify = agent_config.get("ssl_verify", True) + if not ssl_verify: + log.warning( + "%s SSL verification disabled for agent '%s'. " + "This should only be used in development environments.", + self.log_identifier, + agent_name, + ) httpx_client_for_agent = httpx.AsyncClient( timeout=httpx.Timeout( connect=agent_timeout, @@ -1292,6 +1308,7 @@ async def _get_or_create_a2a_client( pool=agent_timeout, ), headers=task_headers if task_headers else None, + verify=ssl_verify, ) if task_headers: diff --git a/src/solace_agent_mesh/agent/proxies/a2a/config.py b/src/solace_agent_mesh/agent/proxies/a2a/config.py index 26c2e278b3..6572f578c3 100644 --- a/src/solace_agent_mesh/agent/proxies/a2a/config.py +++ b/src/solace_agent_mesh/agent/proxies/a2a/config.py @@ -217,6 +217,11 @@ class A2AProxiedAgentConfig(ProxiedAgentConfig): "task_headers cannot override authentication headers. For custom authentication, " "omit the 'authentication' config and use task_headers to set auth headers directly.", ) + ssl_verify: bool = Field( + default=True, + description="SSL certificate verification. Set to False to disable " + "verification for self-signed certificates.", + ) convert_progress_updates: bool = Field( default=True, description="If true, converts TextPart messages in intermediate TaskStatusUpdateEvents " diff --git a/tests/unit/agent/proxies/a2a/test_config.py b/tests/unit/agent/proxies/a2a/test_config.py index 81ed6fb6c8..16910aef17 100644 --- a/tests/unit/agent/proxies/a2a/test_config.py +++ b/tests/unit/agent/proxies/a2a/test_config.py @@ -1,11 +1,102 @@ -""" -Unit tests for A2A proxy configuration models with separate authentication. -""" +"""Unit tests for A2A proxy configuration models.""" import pytest from pydantic import ValidationError -from solace_agent_mesh.agent.proxies.a2a.config import A2AProxiedAgentConfig +from solace_agent_mesh.agent.proxies.a2a.config import ( + A2AProxiedAgentConfig, + A2AProxyAppConfig, +) + + +class TestA2AProxiedAgentConfigSSLVerify: + """Tests for the ssl_verify configuration option.""" + + def test_ssl_verify_defaults_to_true(self): + """ssl_verify should default to True when not specified.""" + config = A2AProxiedAgentConfig( + name="test-agent", + url="https://example.com/agent", + ) + assert config.ssl_verify is True + + def test_ssl_verify_can_be_set_to_false(self): + """ssl_verify can be explicitly set to False.""" + config = A2AProxiedAgentConfig( + name="test-agent", + url="https://example.com/agent", + ssl_verify=False, + ) + assert config.ssl_verify is False + + def test_ssl_verify_can_be_set_to_true(self): + """ssl_verify can be explicitly set to True.""" + config = A2AProxiedAgentConfig( + name="test-agent", + url="https://example.com/agent", + ssl_verify=True, + ) + assert config.ssl_verify is True + + def test_ssl_verify_with_http_url(self): + """ssl_verify setting is accepted even with HTTP URLs.""" + config = A2AProxiedAgentConfig( + name="test-agent", + url="http://localhost:8080/agent", + ssl_verify=False, + ) + assert config.ssl_verify is False + + def test_ssl_verify_in_full_config(self): + """ssl_verify works alongside other configuration options.""" + config = A2AProxiedAgentConfig( + name="test-agent", + url="https://example.com/agent", + ssl_verify=False, + request_timeout_seconds=120, + use_auth_for_agent_card=True, + ) + assert config.ssl_verify is False + assert config.request_timeout_seconds == 120 + assert config.use_auth_for_agent_card is True + + +class TestA2AProxyAppConfigSSLVerify: + """Tests for ssl_verify in the full app configuration.""" + + def test_proxied_agent_with_ssl_verify_false(self): + """Full app config can include agents with ssl_verify=False.""" + config = A2AProxyAppConfig( + namespace="test/namespace", + proxied_agents=[ + { + "name": "secure-agent", + "url": "https://secure.example.com/agent", + "ssl_verify": True, + }, + { + "name": "self-signed-agent", + "url": "https://self-signed.example.com/agent", + "ssl_verify": False, + }, + ], + ) + assert len(config.proxied_agents) == 2 + assert config.proxied_agents[0].ssl_verify is True + assert config.proxied_agents[1].ssl_verify is False + + def test_proxied_agent_ssl_verify_defaults(self): + """Agents without ssl_verify specified should default to True.""" + config = A2AProxyAppConfig( + namespace="test/namespace", + proxied_agents=[ + { + "name": "default-agent", + "url": "https://example.com/agent", + }, + ], + ) + assert config.proxied_agents[0].ssl_verify is True class TestSeparateAuthenticationFields: