Skip to content

SSRF engine blocks host.openshell.internal in 0.0.91 (regression from 0.0.86) #2478

Description

@zanetworker

Summary

host.openshell.internal is the documented mechanism for sandboxes to reach services on the gateway host (e.g., OTEL collectors, dev servers). In gateway 0.0.86 (podman driver), traffic to host.openshell.internal was allowed by the egress proxy and TCP connections completed successfully. In gateway 0.0.91, the L7 policy engine still allows the request, but the new SSRF engine blocks it because DNS resolution fails for the hostname.

Environment

  • Gateway 0.0.86: podman driver, macOS arm64, supervisor :latest
  • Gateway 0.0.91: same setup, downloaded from GitHub release v0.0.91
  • Podman: applehv, arm64, rootless
  • Sandbox image: ghcr.io/nvidia/openshell-community/sandboxes/base:latest

Reproduction

Gateway 0.0.91 (broken)

# Start gateway
OPENSHELL_PODMAN_SOCKET="$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')" \
openshell-gateway --config gateway-podman.toml

# Create sandbox and add policy for host.openshell.internal
openshell sandbox create --name test-ssrf --provider vertex \
  --env OTEL_EXPORTER_OTLP_ENDPOINT=http://host.openshell.internal:9318 \
  -- sleep 300

openshell policy update test-ssrf \
  --add-endpoint 'host.openshell.internal:9318:read-write:rest:enforce' \
  --binary /usr/bin/node \
  --add-allow 'host.openshell.internal:9318:POST:/**' \
  --wait

# Attempt to reach host.openshell.internal from the sandbox
openshell sandbox exec --name test-ssrf -- \
  /usr/bin/node -e "fetch('http://host.openshell.internal:9318/status').then(r=>r.text()).then(console.log).catch(e=>console.log(e.message))"
# Result: hangs (connection never completes)

Sandbox logs show the root cause

[sandbox] HTTP:POST [INFO] ALLOWED /usr/local/bin/claude(72) -> POST http://host.openshell.internal:9318/v1/logs [policy:allow_host_openshell_internal_9318 engine:l7]
[sandbox] HTTP:POST [MED]  DENIED  /usr/local/bin/claude(72) -> POST http://host.openshell.internal:9318/v1/logs [policy:allow_host_openshell_internal_9318 engine:ssrf] [reason:DNS resolution failed for host.openshell.internal:9318: failed to lookup address information: Name does not resolve]

The L7 engine correctly matches the policy and allows the request. The SSRF engine then blocks it because host.openshell.internal cannot be resolved via DNS inside the sandbox.

/etc/hosts inside the sandbox (0.0.91)

# hosts file (comment from base image, sandbox is running on podman not K8s)
127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
fe00::0         ip6-localnet
fe00::0         ip6-mcastprefix
fe00::1         ip6-allnodes
fe00::2         ip6-allrouters
10.129.3.208    default--test-ssrf

No host.openshell.internal entry.

Gateway 0.0.86 (worked)

The same test with gateway 0.0.86 on the same podman setup (same config, same base image) successfully delivered OTEL data. The sandbox's /etc/hosts also did NOT contain host.openshell.internal, which means the 0.0.86 proxy resolved the hostname internally without relying on DNS. Evidence:

  • OTEL receiver on the host received 1,308 bytes at POST /v1/logs from Claude Code running inside the sandbox
  • Sandbox logs showed ALLOWED with no SSRF denial

Root cause analysis

In 0.0.86, the egress proxy handled host.openshell.internal as a special hostname (resolved to the gateway host IP internally, similar to how inference.local is handled). The 0.0.91 SSRF engine introduced a DNS validation step that runs after the L7 policy check. Since host.openshell.internal is a proxy-internal name (not in DNS), the SSRF engine rejects it even though the L7 engine approved it.

Impact

Any use case that relies on sandbox-to-host communication via host.openshell.internal is broken on 0.0.91:

  • OTEL/telemetry forwarding from sandboxes to a host-side collector
  • Development servers running on the host
  • Any service exposed on the gateway host that sandboxes need to reach

Expected behavior

host.openshell.internal should be treated as a trusted proxy-internal hostname by the SSRF engine (same as inference.local). The SSRF engine should either:

  1. Skip DNS validation for known proxy-internal hostnames (host.openshell.internal, inference.local), or
  2. Resolve host.openshell.internal to the gateway host IP before the SSRF check runs

Additional context: K8s egress question

On the Kubernetes driver (workload cluster), we observed a related pattern: the L7 engine allows traffic to policy-approved external endpoints (ClusterIPs, OpenShift routes, NodePort IPs), but TCP connections hang indefinitely. The proxy appears unable to establish outbound TCP connections from the sandbox's network namespace.

Is this by design (all external access on K8s is gateway-mediated, like inference.local)? Or should policy-approved endpoints be reachable via direct TCP on K8s as well? The answer determines whether an OTEL relay endpoint (routed through the gateway gRPC channel) is needed for K8s sandboxes, or whether fixing the TCP connectivity is the right path.

K8s egress logs (for reference)

# Policy allows, but TCP hangs:
[sandbox] NET:OPEN  [INFO] ALLOWED /usr/bin/node(66) -> 172.30.216.186:9318 [policy:allow_172_30_216_186_9318 engine:opa]
[sandbox] HTTP:GET  [INFO] ALLOWED GET http://172.30.216.186:9318/status [policy:allow_172_30_216_186_9318 engine:l7]
# Connection never completes. Same behavior for ClusterIP, NodePort, and HTTP routes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions