Agent Diagnostic
- Loaded skill:
openshell-cli for sandbox/log inspection.
- Ran:
openshell sandbox list and openshell logs <sandbox> against an active sandbox where a previously-working hostname rule started rejecting traffic. Observed a clean transition from allow to deny for the same (binary, host, port):
NET:OPEN [INFO] ALLOWED <python>(499) -> internal-api.example.com:443
[policy:allow_internal_api_example_com_443 engine:opa]
...
NET:OPEN [MED] DENIED <python>(499) -> internal-api.example.com:443
[policy:- engine:ssrf]
[reason:internal-api.example.com resolves to <ip> which is not in allowed_ips, co...]
Same binary, same host, same port — the OPA L4 check still matches the existing rule, but the SSRF override layer rejects the connection because the resolved IP is no longer in the rule's allowed_ips list. The most likely trigger here was a network/VPN session refresh causing DNS to return a different load-balanced backend.
- Found in code:
crates/openshell-sandbox/src/proxy.rs:1964 — for hostname rules with allowed_ips, the proxy re-resolves on every CONNECT and validates each resolved IP via allowed_ips.iter().any(|net| net.contains(&addr.ip())). A miss returns ... resolves to <ip> which is not in allowed_ips, connection rejected (proxy.rs:1966–1970), which matches the observed reason string verbatim.
crates/openshell-sandbox/src/mechanistic_mapper.rs:56 (generate_proposals) groups denial summaries by (host, port, binary) and emits a deterministic rule name via generate_rule_name(host, port) (lines 75, 248–255). The proto comment states "DB-level dedup on (sandbox_id, host, port, binary) handles collisions." When an approved rule already exists for that key, the proposal is suppressed at persistence — no draft chunk is created.
crates/openshell-tui/src/ui/sandbox_detail.rs:39-48 — the "X pending network rule(s)" prompt is sourced from app.sandbox_draft_counts (populated by GetDraftPolicy in crates/openshell-tui/src/lib.rs:2308-2335), which reads persisted pending chunks. The TUI does not subscribe to a live denial stream.
- Tried:
- Verified in the TUI that no pending rule prompt appeared for the denied connection.
- Confirmed via
openshell logs <sandbox> that the denial is being emitted to OCSF (engine:ssrf), so the data exists at the sandbox boundary; it just doesn't reach a surface the operator looks at during normal use.
- Conclusion: The denial is observable in raw logs but invisible in the in-product remediation UX, because the mapper's dedup design treats this as "already covered" rather than "covered host, stale
allowed_ips."
Description
Actual behavior: When a sandbox has an approved network rule for host:port with an explicit allowed_ips allowlist, and a connection to that host is denied because the resolved IP is not in allowed_ips, the TUI shows no pending rule, no banner, and no count change. The denial is only visible in raw logs (engine:ssrf in OCSF). From the operator's view, the request was rejected by a rule they themselves approved, and the in-product remediation workflow surfaces nothing to act on.
Expected behavior: When a denied connection's (host, port, binary) matches an existing approved rule but the resolved IP is not covered by allowed_ips, the TUI should produce some operator-visible signal (a pending rule, a "stale allowed_ips" banner, a denial counter, etc.) so the operator can extend the rule without parsing raw logs.
Reproduction Steps
- Create a sandbox with a policy that has a rule for a hostname whose DNS resolution can change between sessions (e.g., a hostname behind a load-balanced or VPN-routed endpoint), with
allowed_ips pinned to specific IPs:
network_policies:
example:
endpoints:
- host: internal-api.example.com
port: 443
allowed_ips:
- "10.0.5.10"
- "10.0.5.11"
binaries:
- { path: /usr/bin/curl }
- From the sandbox, make a request to
https://internal-api.example.com/... — confirm it succeeds while the resolved IP is in the list. Logs show engine:opa ALLOWED.
- Cause the resolution to drift to an IP outside the list (e.g., reset the host's network/VPN session so DNS returns a different backend).
- Re-run the request from the sandbox — it is rejected with
... resolves to <ip> which is not in allowed_ips, connection rejected. Logs show engine:ssrf DENIED.
- Open the TUI (
openshell term) and select the sandbox.
Observed: No pending rule appears for the denied connection. Pending count is unchanged.
Environment
- OS: macOS 26.3.1 (build 25D771280a)
- Docker: 29.3.0
- OpenShell: 0.0.37-dev.139+g028763d4
- Gateway deployment: local Docker (
docker-dev gateway, http://127.0.0.1:18080)
Logs
[ts] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED <python>(499) -> internal-api.example.com:443 [policy:allow_internal_api_example_com_443 engine:opa]
[ts] [sandbox] [OCSF ] [ocsf] HTTP:POST [INFO] ALLOWED POST http://internal-api.example.com:443/v1/chat/completions
...
[ts] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED <python>(499) -> internal-api.example.com:443 [policy:- engine:ssrf] [reason:internal-api.example.com resolves to <ip> which is not in allowed_ips, co...]
[ts] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED <python>(499) -> internal-api.example.com:443 [policy:- engine:ssrf] [reason:internal-api.example.com resolves to <ip> which is not in allowed_ips, co...]
Agent-First Checklist
Agent Diagnostic
openshell-clifor sandbox/log inspection.openshell sandbox listandopenshell logs <sandbox>against an active sandbox where a previously-working hostname rule started rejecting traffic. Observed a clean transition from allow to deny for the same(binary, host, port):allowed_ipslist. The most likely trigger here was a network/VPN session refresh causing DNS to return a different load-balanced backend.crates/openshell-sandbox/src/proxy.rs:1964— for hostname rules withallowed_ips, the proxy re-resolves on every CONNECT and validates each resolved IP viaallowed_ips.iter().any(|net| net.contains(&addr.ip())). A miss returns... resolves to <ip> which is not in allowed_ips, connection rejected(proxy.rs:1966–1970), which matches the observed reason string verbatim.crates/openshell-sandbox/src/mechanistic_mapper.rs:56(generate_proposals) groups denial summaries by(host, port, binary)and emits a deterministic rule name viagenerate_rule_name(host, port)(lines 75, 248–255). The proto comment states "DB-level dedup on(sandbox_id, host, port, binary)handles collisions." When an approved rule already exists for that key, the proposal is suppressed at persistence — no draft chunk is created.crates/openshell-tui/src/ui/sandbox_detail.rs:39-48— the "X pending network rule(s)" prompt is sourced fromapp.sandbox_draft_counts(populated byGetDraftPolicyincrates/openshell-tui/src/lib.rs:2308-2335), which reads persisted pending chunks. The TUI does not subscribe to a live denial stream.openshell logs <sandbox>that the denial is being emitted to OCSF (engine:ssrf), so the data exists at the sandbox boundary; it just doesn't reach a surface the operator looks at during normal use.allowed_ips."Description
Actual behavior: When a sandbox has an approved network rule for
host:portwith an explicitallowed_ipsallowlist, and a connection to that host is denied because the resolved IP is not inallowed_ips, the TUI shows no pending rule, no banner, and no count change. The denial is only visible in raw logs (engine:ssrfin OCSF). From the operator's view, the request was rejected by a rule they themselves approved, and the in-product remediation workflow surfaces nothing to act on.Expected behavior: When a denied connection's
(host, port, binary)matches an existing approved rule but the resolved IP is not covered byallowed_ips, the TUI should produce some operator-visible signal (a pending rule, a "stale allowed_ips" banner, a denial counter, etc.) so the operator can extend the rule without parsing raw logs.Reproduction Steps
allowed_ipspinned to specific IPs:https://internal-api.example.com/...— confirm it succeeds while the resolved IP is in the list. Logs showengine:opaALLOWED.... resolves to <ip> which is not in allowed_ips, connection rejected. Logs showengine:ssrfDENIED.openshell term) and select the sandbox.Observed: No pending rule appears for the denied connection. Pending count is unchanged.
Environment
docker-devgateway,http://127.0.0.1:18080)Logs
Agent-First Checklist
openshell-cli)