Skip to content

fix(strands-agent-base): CVE remediation + AccessDenied retry + A2A factory fix - #36

Open
shapirov103 wants to merge 4 commits into
mainfrom
feature/oam-for-agents
Open

fix(strands-agent-base): CVE remediation + AccessDenied retry + A2A factory fix#36
shapirov103 wants to merge 4 commits into
mainfrom
feature/oam-for-agents

Conversation

@shapirov103

Copy link
Copy Markdown
Contributor

Summary

Hardens the strands-agent-base image: dependency/base-image CVE remediation, retry-with-backoff for the first-boot IAM propagation race, and two A2A SDK compatibility fixes discovered while testing on the hub cluster.

Also includes an earlier commit hardening the aws-service-identity OAM trait's init container (control-plane readiness gate on PodIdentity via kubectl wait, replacing an STS-based check that could be fooled by node-role fallback).

Changes

Dependency/base-image hardening (pyproject.toml, Dockerfile):

  • Pin fastapi==0.139.2, starlette==1.3.1 — fixes CVE-2025-62727, CVE-2026-48818, CVE-2026-54283
  • Pin pydantic==2.13.4, uvicorn==0.51.0, bedrock-agentcore==1.18.1 (previously unpinned), aws-opentelemetry-distro==0.18.0, strands-agents==1.48.0
  • Add tenacity==9.1.4 for retry-with-backoff
  • Pin Dockerfile builder image (uv:0.9.30-python3.13-bookworm-slim) and final stage (python:3.13-slim-bookworm by digest) for supply-chain immutability
  • Remaining OS-layer CVEs (zlib1g, perl, libsqlite3-0, util-linux) are Debian bookworm will_not_fix/fix_deferred/no-dsa — not resolvable via apt upgrade

Resilience (app/agent.py):

  • tenacity.retry(...) on _construct_agent, retrying only on botocore.exceptions.ClientError with code AccessDeniedException (the first-boot IAM/Pod-Identity propagation race), exponential backoff (multiplier=1, max=16s), capped at 6 attempts
  • Skip AgentCore session-manager attachment when the session id is the A2A agent-card placeholder (__agent_card__) — that probe agent is never used for real requests, and the placeholder fails AgentCore's sessionId validation regex (must start with alphanumeric)

A2A SDK compatibility (app/main.py):

  • Switch A2AServer from the deprecated single shared agent= (not multi-tenant-safe; rejected outright by strands-agents 1.48's StrandsA2AExecutor when a session_manager is attached) to agent_factory=create_agent, giving each A2A context its own context-scoped agent + session manager

Testing

  • Rebuilt multi-arch (amd64 + arm64) locally with podman, verified boot + /health on both arches
  • Re-scanned with trivy: starlette, fastapi, tenacity, bedrock_agentcore, pydantic, uvicorn all show 0 vulnerabilities post-fix
  • Pushed to public.ecr.aws/z0a4o2j5/strands-agent (:latest, :v1.1.0-security)
  • Deployed to the hub cluster (peeks-hub): all 3 my-agent pods Running 1/1, 0 restarts, Rollout phase Healthy — no AccessDenied crashes, no ValueError/ValidationException crashes

Not in scope

  • Residual Debian bookworm OS-layer CVEs (documented as accepted; not fixable without a distro-version jump)
  • Platform-wide default resources/startupProbe for the agent OAM component (flagged as a follow-up, not applied here beyond the one example)

ADRs for secretless workload identity (Shape A gateway identity; OAM identity
traits; aws-service-identity/XPodIdentity/env-config; per-cluster eks_oidc_provider)
and the user-delegated token-exchange roadmap, incl. the pending agentgateway
release that ships backend.auth.oauthTokenExchange (v1.1.0 lacks it; PRs #2189/#2458).
…nt resource requests

aws-service-identity trait: replace the STS-based init wait with a distroless,
digest-pinned Chainguard kubectl 'wait --for=condition=Ready' on the PodIdentity
resource, plus a namespaced Role/RoleBinding (get/list/watch podidentities) so the
pod ServiceAccount can read it. Control-plane readiness only; the brief IAM
data-plane propagation window is handled by app-level retry.

example-agent-agentcore-memory: add CPU/memory requests (+ mem limit) so the agent
isn't CPU-starved on contended nodes (root cause of the liveness-kill crashloops).

Regenerated oam-agent-components templates.
…ctory fix

Dependency/base-image hardening:
- Pin fastapi==0.139.2, starlette==1.3.1 (fixes CVE-2025-62727, CVE-2026-48818,
  CVE-2026-54283), pydantic==2.13.4, uvicorn==0.51.0, bedrock-agentcore==1.18.1
  (previously unpinned), aws-opentelemetry-distro==0.18.0, strands-agents==1.48.0.
- Add tenacity==9.1.4 for retry-with-backoff (replaces hand-rolled loop).
- Pin Dockerfile builder (uv:0.9.30-python3.13-bookworm-slim) and final stage
  (python:3.13-slim-bookworm by digest) for supply-chain immutability.
- Remaining OS-layer CVEs (zlib1g, perl, libsqlite3-0, util-linux) are Debian
  bookworm will_not_fix/fix_deferred/no-dsa - not resolvable via apt upgrade.

Resilience:
- agent.py: tenacity retry() on _construct_agent, retrying only on botocore
  AccessDeniedException (first-boot IAM/Pod-Identity propagation race),
  exponential backoff (multiplier=1, max=16s), capped at 6 attempts.
- agent.py: skip AgentCore session-manager attachment for the A2A agent-card
  placeholder context id (__agent_card__) - that probe agent is never used for
  real requests, and the placeholder fails AgentCore's sessionId validation
  regex (must start with alphanumeric).
- main.py: switch A2AServer from the deprecated single shared agent param (not
  multi-tenant-safe, and rejected by strands-agents 1.48's StrandsA2AExecutor
  when a session_manager is attached) to the agent_factory param, giving
  each A2A context its own context-scoped agent + session_manager.

Verified: rebuilt multi-arch (amd64+arm64), re-scanned with trivy (starlette/
fastapi/tenacity/etc. all 0 vulnerabilities), pushed to
public.ecr.aws/z0a4o2j5/strands-agent (:latest, :v1.1.0-security), and
confirmed on the hub cluster - all 3 my-agent pods Running 1/1, 0 restarts,
Rollout Healthy.
…oken expires

The MCP client opened one persistent connection per server at first use and
reused it indefinitely (_mcp_exit_stack), authenticated with the
gateway-identity workload token read at connect time. That token has a fixed
1h TTL (expirationSeconds=3600 on the projected ServiceAccount token). The
kubelet rotates the token file on disk, but the already-open MCP connection
never re-reads it, so any tool call issued after ~1h of pod uptime failed
with a 401 from agentgateway ("Error(ExpiredSignature)"), surfacing to the
user as "unable to access the time tool due to a connection issue."

Fix: track each MCPClient instance (not just an ExitStack) and, once a
connection exceeds _MCP_CONNECTION_MAX_AGE_SECONDS (45 min, under the 1h
TTL), call stop() then start() on the SAME instance rather than creating a
new one. This re-invokes the transport callable (and therefore
_gateway_headers(), which reads the token file fresh) while preserving
object identity - required because MCPClient.call_tool_async is bound to
self and tool objects returned by list_tools_sync() delegate through the
client instance, not a frozen session. Any already-cached Agent (the
session cache in get_or_create_agent is unbounded and never evicted) keeps
working after a recycle instead of holding a reference to a client that
would otherwise need replacing.

Verified via a mocked unit test: after forcing a near-zero max age, the
second call recycles via stop()+start() on the identical object (checked
with `is`), not a new instance, with the tools list unchanged. Rebuilt and
pushed a multi-arch image (public.ecr.aws/z0a4o2j5/strands-agent :latest,
:v1.2.0-mcp-recycle) and redeployed both default/oap-assistant-a and
agents/my-agent; confirmed a live mcp-time tool call succeeds end-to-end
with no regression.
@elamaran11

Copy link
Copy Markdown
Contributor

@shapirov103 I see merge conflicts fix those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants