Problem Statement
When a network policy endpoint is declared by hostname (e.g., mcp-internal.corp.example.com:8443), and that hostname resolves to an RFC 1918 private IP (e.g., 10.0.1.42), the SSRF protection layer blocks the connection before the OPA policy engine evaluates it:
DENIED /sandbox/.local/share/claude/versions/2.1.150(999) -> mcp-internal.corp.example.com:8443
[policy:- engine:ssrf]
[reason:mcp-internal.corp.example.com resolves to internal address 10.0.1.42, connection rejected]
The current two-step approval flow (declare hostname, get SSRF denial, then manually add allowed_ips) makes sense for policy advisor proposals, where the system suggests endpoints it observed and the user has not explicitly approved. However, for endpoints the user has explicitly declared in their policy, the hostname declaration already represents a trust decision. Requiring a separate allowed_ips entry for the same endpoint adds friction without additional security value, since the admin already chose to allow that host.
This affects enterprise deployments where internal services (MCP servers, API gateways, registries) are reachable via DNS names that resolve to private IPs.
Proposed Design
Distinguish between advisor-proposed endpoints (where the two-step flow is appropriate) and user-declared endpoints (where the hostname itself is the trust signal).
When the SSRF layer resolves a destination hostname to a private IP, it should check whether that hostname:port pair is explicitly declared in a loaded policy endpoint (not proposed by the advisor). If so, treat the connection as trusted and bypass the SSRF check for that specific connection.
This keeps the existing two-step flow for advisor proposals intact while eliminating the redundant allowed_ips requirement for endpoints the user has already explicitly declared.
Implementation sketch:
- The proxy already resolves DNS and checks
allowed_ips post-resolution
- At that check point, add a lookup against the loaded policy's declared endpoint hostnames
- If the destination hostname:port matches a declared endpoint, allow the resolved private IP
- Loopback (127.0.0.0/8), link-local (169.254.0.0/16), and unspecified (0.0.0.0) remain always-blocked, even for declared endpoints
Alternatives Considered
-
Manual allowed_ips: Works today but is fragile (IPs change), non-obvious (hostname already declared as trusted), and creates maintenance burden. The docs at docs/sandboxes/policy-advisor.mdx already note that "proposals do not add allowed_ips automatically," but this two-step makes less sense for user-declared endpoints.
-
Auto-resolve at policy load time: Resolve hostnames and populate allowed_ips when the policy is loaded. Simpler but breaks when DNS changes between policy load and connection time.
-
Explicit ssrf_exempt flag: Add a boolean to the endpoint schema. Gives explicit control but adds schema complexity for a case that should be the default behavior for declared endpoints.
-
Disabling SSRF globally: Defeats the purpose. The protection is valuable for undeclared/unknown destinations.
Agent Investigation
Investigated the SSRF enforcement path in the codebase:
crates/openshell-sandbox/src/mechanistic_mapper.rs (lines 51-54): Documents the intentional two-step flow: "Proposals never include allowed_ips. If the user applies a proposed rule and the host resolves to a private IP, the proxy's SSRF defense will deny the connection." This is the right behavior for proposals but overly cautious for user-declared endpoints.
crates/openshell-sandbox/data/sandbox-policy.rego (lines 114-123): OPA policy already handles allowed_ips for hostless endpoints. SSRF check happens in Rust post-DNS-resolution.
docs/reference/policy-schema.mdx (line 164): Documents allowed_ips as "CIDR or IP allowlist for SSRF override."
docs/security/best-practices.mdx (line 135): Recommends allowed_ips for known internal services. The proposed change would eliminate this manual step for declared endpoints.
- Loopback/link-local/unspecified remain always-blocked per
docs/security/best-practices.mdx line 134, and this proposal does not change that.
Problem Statement
When a network policy endpoint is declared by hostname (e.g.,
mcp-internal.corp.example.com:8443), and that hostname resolves to an RFC 1918 private IP (e.g.,10.0.1.42), the SSRF protection layer blocks the connection before the OPA policy engine evaluates it:The current two-step approval flow (declare hostname, get SSRF denial, then manually add
allowed_ips) makes sense for policy advisor proposals, where the system suggests endpoints it observed and the user has not explicitly approved. However, for endpoints the user has explicitly declared in their policy, the hostname declaration already represents a trust decision. Requiring a separateallowed_ipsentry for the same endpoint adds friction without additional security value, since the admin already chose to allow that host.This affects enterprise deployments where internal services (MCP servers, API gateways, registries) are reachable via DNS names that resolve to private IPs.
Proposed Design
Distinguish between advisor-proposed endpoints (where the two-step flow is appropriate) and user-declared endpoints (where the hostname itself is the trust signal).
When the SSRF layer resolves a destination hostname to a private IP, it should check whether that hostname:port pair is explicitly declared in a loaded policy endpoint (not proposed by the advisor). If so, treat the connection as trusted and bypass the SSRF check for that specific connection.
This keeps the existing two-step flow for advisor proposals intact while eliminating the redundant
allowed_ipsrequirement for endpoints the user has already explicitly declared.Implementation sketch:
allowed_ipspost-resolutionAlternatives Considered
Manual
allowed_ips: Works today but is fragile (IPs change), non-obvious (hostname already declared as trusted), and creates maintenance burden. The docs atdocs/sandboxes/policy-advisor.mdxalready note that "proposals do not addallowed_ipsautomatically," but this two-step makes less sense for user-declared endpoints.Auto-resolve at policy load time: Resolve hostnames and populate
allowed_ipswhen the policy is loaded. Simpler but breaks when DNS changes between policy load and connection time.Explicit
ssrf_exemptflag: Add a boolean to the endpoint schema. Gives explicit control but adds schema complexity for a case that should be the default behavior for declared endpoints.Disabling SSRF globally: Defeats the purpose. The protection is valuable for undeclared/unknown destinations.
Agent Investigation
Investigated the SSRF enforcement path in the codebase:
crates/openshell-sandbox/src/mechanistic_mapper.rs(lines 51-54): Documents the intentional two-step flow: "Proposals never includeallowed_ips. If the user applies a proposed rule and the host resolves to a private IP, the proxy's SSRF defense will deny the connection." This is the right behavior for proposals but overly cautious for user-declared endpoints.crates/openshell-sandbox/data/sandbox-policy.rego(lines 114-123): OPA policy already handlesallowed_ipsfor hostless endpoints. SSRF check happens in Rust post-DNS-resolution.docs/reference/policy-schema.mdx(line 164): Documentsallowed_ipsas "CIDR or IP allowlist for SSRF override."docs/security/best-practices.mdx(line 135): Recommendsallowed_ipsfor known internal services. The proposed change would eliminate this manual step for declared endpoints.docs/security/best-practices.mdxline 134, and this proposal does not change that.