Security model

The four guard families that make adk-rs secure by default, what trips them, and how to opt out deliberately.

adk-rs is secure by default: a handful of guards trip whenever behaviour would be unsafe — sending credentials over plaintext HTTP, exposing an unauthenticated control plane to the network, writing attacker-controlled paths, or running model-generated code with host privileges. Each guard has exactly one deliberate opt-out, and the crate as a whole is #![forbid(unsafe_code)].

1. HTTPS-only credentials

Every credential-bearing outbound client routes its destination through transport_security::require_secure_url(url, field). The rule: the URL must be https:// (case-insensitive) or a loopback http:// host. Anything else is rejected at construction time, before any request is built.

  • Who checks: the provider clients (Gemini, Anthropic, OpenAi), RestApiTool (OpenAPI tools), the MCP HTTP transport, the A2A client when its extra headers look credential-bearing (Authorization, Cookie, Proxy-Authorization, x-api*, x-auth* — the classification is the public helper transport_security::header_looks_credential_bearing(name)), and A2A push-notification webhook URLs.
  • Loopback exemption: localhost, any 127.0.0.0/8 address, and [::1] pass — this is what lets tests and local mocks (e.g. wiremock, Ollama) work over plain HTTP.
  • No parser differential: is_secure_url parses with the WHATWG url crate — the same parser reqwest uses — so the validated host is exactly the host the request goes to. http://127.0.0.1@evil.example.com is rejected, and so is http://evil.com\@localhost/: in WHATWG http(s) URLs the \ terminates the authority, so the real host is evil.com. Unknown schemes (ftp://, file://, scheme-less strings) always fail.
  • No redirect leaks: every credential-bearing outbound client disables HTTP redirects, because reqwest re-sends custom headers like x-api-key to redirect targets — the three provider clients (and their embedders), the MCP HTTP transport, the A2A client and its agent-card fetch, RestApiTool, the A2A push notifier, and the OAuth token clients.
  • Secret-safe errors: the error message names the offending field (e.g. OpenAiConfig.base_url), never the URL itself, because URLs can carry secrets in their userinfo.

2. Loopback-only dev servers

Both serve entry points — the HTTP server (adk_rs::server::serve_with) and the A2A bridge (adk_rs::a2a::serve_with) — refuse to bind a non-loopback address when no auth token is configured. The error is immediate and explicit; nothing listens.

  • Opt-out 1 (preferred): configure a bearer token — AppState::with_bearer_token(...) for the HTTP server, A2aServerConfig::with_bearer_token(...) for A2A, or --auth-token / ADK_WEB_TOKEN on the CLI. Every request must then carry Authorization: Bearer <token>.
  • Opt-out 2 (deliberate): pass ServeOptions { dangerously_allow_unauthenticated_remote: true } (CLI: --dangerously-allow-unauthenticated-remote). The name is the warning.
  • Never silent: even with auth configured, binding a non-loopback interface logs a warn line describing exactly who can now reach the agents.

Token comparison is constant-time: the middleware XOR-folds the byte difference across the full token rather than short-circuiting, so timing cannot leak a prefix match. (Length mismatch returns early — token length is fixed per deployment, not a secret.) Failed auth returns 401 with a WWW-Authenticate: Bearer header.

3. Filesystem artifact path sanitization

FileArtifactService (artifacts) builds paths as <root>/<app>/<user>/<session>/<filename>/vNNNNNN.json — four components that can all be attacker-influenced through the HTTP API. Each component is passed through sanitize, which:

  • replaces every character that is not alphanumeric, _, -, or . with _,
  • rewrites dot-only components (., .., ..., …) to _, so .. segments can never climb out of the artifact root, and
  • collapses empty input to _, because Path::join("") is a no-op that would silently merge two adjacent components.

There is no opt-out: sanitization is unconditional on the filesystem backend. If you need raw names, store them in artifact metadata, not in the path.

4. Locked-down container code execution

ContainerCodeExecutor (feature code-exec-docker, see Code execution) runs each call in a fresh ephemeral container with a hardened docker run argv by default:

FlagDefaultRelax with
--network=noneno outbound networkwith_extra_args (deliberate)
--read-only + --tmpfs=/tmp:rw,exec,size=64mread-only rootfs, small writable /tmpwith_extra_args
--memory / --memory-swap256m (swap pinned to the same value)with_memory("1g")
--cpus1.0with_cpus("0.5")
--pids-limit128 (fork-bomb cap)with_pids_limit(n)
--user65534:65534 (nobody, never root)with_user("uid:gid")
--cap-drop=ALL + --security-opt=no-new-privilegesonthe drop_capabilities field

Every cap is a typed with_* builder method, so loosening the sandbox is an explicit, reviewable line of code. The argv builder (build_run_args) is public so tests can assert the policy. By contrast, LocalCodeExecutor is subprocess isolation only — the crate documents it as not a security boundary.

What trips, at a glance

You do thisWhat happensDeliberate opt-out
Point a provider / RestApiTool / MCP / A2A client with credentials at http://api.example.comError::Config at constructionUse HTTPS or a loopback proxy
serve on 0.0.0.0 with no tokenError::Config, nothing bindsBearer token, or dangerously_allow_unauthenticated_remote
Register an A2A push webhook at a public http:// URLJSON-RPC -32602 INVALID_PARAMSUse an HTTPS webhook receiver
Send filename: "../../etc/cron.d/x" to the artifact APIComponent rewritten to _; write stays under the rootNone
Run model-emitted code in DockerNo network, read-only rootfs, nobody user, resource capsTyped with_* builders

  • HTTP serverServeOptions and the bearer middleware in context.
  • A2A protocol — the second server with the same bind policy.
  • Code execution — executor APIs and retry behaviour.
  • Auth — credential storage and OAuth flows (a separate concern from transport security).