Skip to content

bug: TUI shows no pending rule when a denial is for an existing rule whose allowed_ips is stale #1245

@maxdubrinsky

Description

@maxdubrinsky

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

  1. 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 }
  2. 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.
  3. 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).
  4. 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.
  5. 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

  • I pointed my agent at the repo and had it investigate this issue
  • I loaded relevant skills (openshell-cli)
  • My agent could not resolve this — the diagnostic above explains why

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions