feat: HTTP-level ACL for AI agent sandboxing#7
Merged
congwang-mk merged 26 commits intomainfrom Apr 5, 2026
Merged
Conversation
bc21452 to
176935a
Compare
…evel ACL Add HTTP access control data structures and matching logic to the policy module. This includes HttpRule parsing from "METHOD host/path" format, glob-based path matching, and an ACL check function with deny-first evaluation semantics. Fields added to Policy and PolicyBuilder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Change http_allow and http_deny builder methods to propagate parse errors using expect() instead of silently dropping malformed rules. This is critical for security as silently ignoring a rule could bypass intended restrictions. 2. Rename glob_match to prefix_or_exact_match to accurately reflect the limited matching capabilities. Update doc comment to clarify that only trailing '*' is supported, not mid-pattern wildcards. All existing tests pass with the updated names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HTTP/HTTPS MITM proxy library built on hyper + tokio-rustls + rcgen. Will be used to implement the HTTP ACL proxy in the next task. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add http_acl module with AclHandler (hudsucker HttpHandler) that enforces HTTP ACL allow/deny rules via http_acl_check, and spawn_http_acl_proxy() that generates a CA cert, binds to a random local port, and spawns the proxy on the tokio runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add http_acl_addr to SupervisorState and has_http_acl to NotifPolicy so the supervisor intercepts connect() when HTTP ACL is active. In connect_on_behalf(), redirect port 80/443 connections to the proxy address while preserving the existing IP allowlist check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spawn the HTTP ACL proxy before fork when http_allow/http_deny rules are configured, inject SSL_CERT_FILE into the child environment, add the CA cert path to fs_readable, and set http_acl_addr on the supervisor state so connect() can redirect traffic through the proxy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add http_allow and http_deny fields to the Run variant with RULE value names - Wire into PolicyBuilder with loops that call builder.http_allow() and builder.http_deny() - Add http_allow and http_deny parameters to validate_no_supervisor() function - Add validation checks to reject --http-allow and --http-deny in --no-supervisor mode - Add http_allow and http_deny to command destructuring pattern in main() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add sandlock_policy_builder_http_allow and sandlock_policy_builder_http_deny functions following the same pattern as existing builder methods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add http_allow and http_deny fields to Policy dataclass - Register FFI functions for http_allow and http_deny builder calls - Wire rules into policy builder construction - Add fields to _HANDLED_FIELDS for validation Format: "METHOD host/path" with glob matching support. When http_allow is set, all other HTTP requests are denied by default. A transparent MITM proxy is spawned in the supervisor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 4 integration tests verifying HTTP ACL proxy behavior end-to-end: - test_http_allow_get: allowed rule permits matching requests - test_http_deny_non_matching: non-matching paths are blocked (403) - test_http_deny_precedence: deny rules take precedence over allow - test_http_no_acl_unrestricted: no rules means unrestricted access All tests are marked #[ignore] since they require network access to httpbin.org. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse http_allow and http_deny from TOML profiles in parse_profile(). Wire HTTP ACL rules from loaded profiles into the CLI builder, converting HttpRule structs back to "METHOD host/path" string format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…TM, wildcards, and more Cover method-level filtering (GET vs POST), HTTPS MITM proxy allow/deny, multiple allow rules, wildcard host with deny precedence, non-HTTP port passthrough, and HTTP ACL combined with net_allow_hosts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One thread for the seccomp notification loop, one for the HTTP ACL proxy. The supervisor is I/O-bound so more threads just waste memory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion - Remove auto-generated CA cert and SSL_CERT_FILE injection - Add --https-ca and --https-key CLI args for user-provided CA - Only intercept port 443 when CA cert is provided; port 80 always intercepted when HTTP ACL rules exist - Add https_ca/https_key to Policy, PolicyBuilder, FFI, and Python SDK - No CA = HTTP-only ACL (port 80). With CA = HTTP + HTTPS (port 80+443) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ey pairing PolicyBuilder.http_allow()/http_deny() no longer panic on invalid input. Rules are stored as raw strings and parsed in build(), which returns PolicyError on malformed rules. Also rejects configurations where only one of --https-ca/--https-key is provided. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoid expensive RSA keygen on every proxy spawn in HTTP-only mode by caching a dummy CA via LazyLock. Add graceful shutdown support: the proxy handle now uses a oneshot channel with with_graceful_shutdown(), and a Drop impl ensures cleanup when the sandbox is dropped. The handle is stored on the Sandbox struct so it lives as long as the child process. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A sandboxed process could bypass HTTP ACL rules by setting a spoofed Host header (e.g. Host: allowed.com) while connecting to a different IP. The proxy had no way to verify the claim. Fix: the supervisor now records a mapping of (local_socket_addr → original_dest_ip) after each redirected connect(). The proxy handler looks up the original destination via ctx.client_addr, resolves the claimed Host to IPs, and rejects the request if none match. This closes the Host header spoofing vector. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The verify_host() check resolves the claimed Host to IPs on every request, which adds latency. Add a TTL-based DNS cache (30s) so repeated requests to the same host skip the lookup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the proxy only intercepted hardcoded ports 80 and 443. HTTP on non-standard ports (e.g. 8080) bypassed ACL entirely. Add --http-port <PORT> (repeatable) to specify which TCP ports the HTTP ACL proxy intercepts. When omitted, defaults to [80] (plus 443 when --https-ca is set). This closes the exfiltration gap on custom ports. Wired through CLI, profiles, FFI, and Python bindings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
policy_for_tool() set net_connect=[] intending "block all", but the SDK treated empty lists as "not set" so Landlock never activated TCP filtering. Fix: use net_connect=[0] and net_bind=[0] (port 0 is never a real target) to activate Landlock network restrictions while allowing nothing. Also enable no_udp for complete network deny-by-default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Support IPv6 connections through HTTP ACL proxy by redirecting via IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) with sockaddr_in6 - Fix orig_dest map memory leak: clean up entries on all 403 paths, not just on allowed requests - Fix TOCTOU race: write orig_dest mapping before connect() by explicitly binding the socket first, eliminating the window where the proxy could receive a request before the mapping exists - Add 4 IPv6 integration tests: allow, deny, non-intercepted port, and remote AAAA connectivity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite all HTTP ACL integration tests to use local TCP servers spawned in Rust test threads — no external network dependency - Fix transparent proxy forwarding: reconstruct absolute URI from Host header so hudsucker knows where to forward allowed requests (was returning 502 for relative URIs like "GET /path") - Fix seccomp notif list: add connect/sendto/sendmsg/bind to notification syscalls when http_allow/http_deny are set (was only triggered by net_allow_hosts, so HTTP ACL without IP allowlist silently bypassed the proxy) - All 12 HTTP ACL tests now run without #[ignore], 0 external deps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add normalize_path() that decodes percent-encoding, collapses duplicate slashes, and resolves . and .. segments before matching against ACL rules. Without this, attackers could bypass deny rules via: /v1//admin/settings (double slash) /v1/../admin/settings (dot-dot traversal) /%61dmin/settings (percent-encoded 'a') Applied to both incoming request paths (in matches()) and rule paths (in parse()) so both sides are in canonical form. Includes 12 new unit tests covering normalization and bypass prevention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add HTTP ACL feature documentation: - CLI examples (--http-allow, --http-deny, --https-ca/--https-key) - Python API example with http_allow/http_deny - Rust API example with http_allow/http_deny - Feature comparison table entry - Policy reference with all HTTP ACL fields - Python SDK README: new HTTP ACL section with parameter table, rule format docs, and code example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3a5f362 to
02bfaa1
Compare
congwang-mk
added a commit
that referenced
this pull request
Apr 5, 2026
Add HTTP access control (--http-allow / --http-deny) that enforces method + host + path rules on HTTP/HTTPS traffic. A transparent MITM proxy (hudsucker) runs inside the supervisor's tokio runtime — child connections to intercepted ports are redirected via seccomp connect() interception. Rule format: "METHOD host/path" with wildcard (*) and trailing-* prefix matching. Deny rules are evaluated first; when allow rules exist, non-matching requests are denied by default. Paths are normalized (percent-decoding, dot-segment resolution, slash collapsing) to prevent ACL bypasses. Key design decisions: - User-provided CA for HTTPS MITM (--https-ca/--https-key) rather than auto-generated certs — explicit trust, no surprise injection - Configurable port interception (--http-port) — defaults to 80, adds 443 when CA is provided - Host header verification via orig_dest map prevents spoofing - DNS cache (30s TTL) on Host verification to reduce latency - Lazy dummy CA for HTTP-only mode avoids per-spawn keygen cost - Graceful proxy shutdown via Drop impl on the proxy handle - IPv6 support via IPv4-mapped addresses (::ffff:127.0.0.1) - TOCTOU-safe: orig_dest written before connect(), not after Wired through all layers: CLI, profiles, FFI, Python SDK, and MCP. Includes 12 integration tests (local servers, no external deps) and 49 unit tests covering rule parsing, matching, normalization, and bypass prevention. Also fixes MCP deny-by-default to actually block TCP via Landlock (net_connect=[0] instead of empty list). Signed-off-by: Cong Wang <cwang@multikernel.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--http-allow/--http-deny) that enforces method + host + path rules on HTTP/HTTPS trafficconnect()interceptionSSL_CERT_FILEExample
Changes across layers
Performance (wrk + nginx, 256B response, 1 thread, 1 connection)
The HTTP ACL proxy adds ~1 us per request (~4% at 36K rps). For AI agent workloads (100ms+ API latency), this is 0.001% overhead — completely invisible.
Test plan
#[ignore]):Generated with Claude Code