Repository navigation
process_binary_aware_network_policy: approved + reloaded draft chunks still reject matching traffic and keep re-drafting the same rule #2752
Description
Summary
process_binary_aware_network_policy's learn-and-approve workflow doesn't actually gate live traffic: approving a draft chunk (via ApproveAllDraftChunks) and confirming the sandbox reloaded the new policy version (ReportPolicyStatus: status=loaded) does not unblock matching connections. A fresh attempt from the exact same binary to the exact same already-approved host:port is still rejected, and — confusingly — generates a brand-new duplicate draft chunk for the identical rule instead of being recognized as already covered.
Reproduction
- Kubernetes driver,
topology = "sidecar",process_binary_aware_network_policy = true(under[openshell.drivers.kubernetes.sidecar]). - Start a workload inside the sandbox (via
ExecSandbox) that makes an outbound HTTP request to an in-cluster service, e.g.http://some-service.ns.svc:8000/v1/..., using the sandbox's ownHTTP_PROXY=http://127.0.0.1:3128(confirmed listening/reachable) andNODE_USE_ENV_PROXY=1. - First attempt fails:
fetch failed/Request was cancelled. Server-side,SubmitPolicyAnalysislogs a new draft chunk, e.g.allow_some_service_ns_svc_8000, scoped to the exact resolved binary path (confirmed viareadlink -f $(which node)matching exactly what's recorded inbinaries=[...]on the rule). - Call
ApproveAllDraftChunks({name: "<sandbox-short-name>", include_security_flagged: true}). Server logsCONFIG:APPROVED ... add-rule allow_some_service_ns_svc_8000 ... binaries=[/usr/local/bin/node]andCONFIG:MERGED, returning a newpolicy_version. - Server logs
ReportPolicyStatus: sandbox reported policy load result ... status=loadedfor that same version shortly after — confirming the sandbox side has fetched and applied the new policy. - Retry the exact same request (same binary, same host, same port):
- Same failure:
fetch failed/Request was cancelled. - A brand-new
SubmitPolicyAnalysisdraft chunk gets submitted for the identical rule (draft_versionincremented), as if the already-approved rule were never consulted.
- Same failure:
We tried, in order, to rule out staleness/propagation-timing explanations — none changed the outcome:
- Relaunching the workload process after approval (same pod): still fails.
- Not relaunching at all (same live process, same TCP stack): still fails.
- Deleting the pod entirely so the driver's reconciler recreates it fresh (new containers, policy fetched from scratch at startup rather than live-reloaded): still fails, and still re-drafts the same already-approved rule.
This suggests the enforcement decision path (in the network-mode supervisor process) isn't actually consulting the same rule set that SubmitPolicyAnalysis/ApproveAllDraftChunks operate on — or there's a scoping key mismatch (e.g. hostname normalization, binary hash vs. path, or a workspace parameter defaulting differently between the two paths) that we couldn't identify from the client side.
Impact
With process_binary_aware_network_policy = true, no amount of legitimate approval workflow usage unblocks real traffic, making the feature effectively non-functional for any egress beyond whatever ships pre-approved. We also tried process_binary_aware_network_policy = false as a way to fall back to plain L3/L4 enforcement — that made things strictly worse: even loopback-adjacent DNS-via-the-local-proxy path broke (getaddrinfo EAI_AGAIN when bypassing the proxy directly, and the proxy path itself started failing more baseline requests, including ones that previously depended on nothing but plain NetworkPolicy L3/L4 matching that we'd already correctly scoped at the Kubernetes level).
Environment
- OpenShell built from
devbranch, commitc4b500a(2026-08-14) - Kubernetes driver,
topology = "sidecar",sa_token_ttl_secs = 3600,enable_user_namespaces = true - Workaround we're evaluating: bypass sandbox-originated outbound network calls entirely for this specific traffic (file-based request/response bridge polled via
ExecSandbox, actual HTTP call made from outside the sandbox).
Activity
johntmyers commentedon Aug 14, 2026
@thoraxe You’re still getting connections denied because the approved chunk targets an endpoint that resolves to private IP space, so the connection is subsequently blocked by the SSRF checks.
Policy-advisor-generated rules deliberately cannot authorize private-address access, even after human approval.
What isn’t clear from the issue is this:
Are you seeing an actual second draft-rule entry with a different chunk ID, or merely a second
SubmitPolicyAnalysisRPC call?If it is only another RPC call, that is expected:
.svchostname resolves to private IP space.SubmitPolicyAnalysiscall for the same host, port, and binary.The second submission should be deduplicated against the existing chunk and should not create a new pending rule in the TUI. Please verify whether the returned chunk ID changes and whether a new pending rule actually appears. The server calculates/logs a new
draft_versionbefore the persistence upsert, so the incremented version alone does not prove that a second row was created.Please also check the OCSF logs. I expect the retry to show the network-policy decision as allowed, followed by an
engine:ssrfdenial indicating that the hostname resolved to a private/internal address.If the workload needs to reach private services, declare the
.svchostname directly in the sandbox policy instead of relying on a policy-advisor proposal. An exact hostname explicitly declared by the operator may resolve to private IP space without requiringallowed_ips. You can also usehost + allowed_ipsif you want to constrain the permitted private addresses. Loopback, link-local, unspecified addresses, and protected control-plane ports remain blocked.thoraxe commentedon Aug 14, 2026
Thanks for the tips -- I'll dig in and follow-up.
thoraxe commentedon Aug 17, 2026
Reproduced this directly against the gateway's real API to check whether this is SSRF rejection-after-approval vs. non-functional enforcement.
Setup: gateway/chart
0.0.104, relevant config:Repro script (Python, via the generated
openshell_pb2/openshell_pb2_grpcstubs, mTLS + a Keycloak-issued bearer JWT withaud: [forge-api, openshell-cli]):Output, all 3 attempts identical:
(
403is the vLLM proxy's own auth rejection — proof of a completed TCP+TLS+HTTP round trip, not a network-level block. NoERR, no timeout, ever.)Gateway logs (ANSI stripped, noise from unrelated RPCs filtered out):
A policy was confirmed
loadedon the sandbox before the first attempt, andSubmitPolicyAnalysisclearly runs (drafting chunks,draft_versionincrementing 1→2) — but it only fires after each connection has already completed successfully, and never blocked, delayed past ~10s, or required approval for any of the three identical attempts to a private/undeclared endpoint. This doesn't match the SSRF-rejection-after-approval pattern (which predicts a deny on retry); we saw zero denials.johntmyers commentedon Aug 18, 2026
@thoraxe can you provide the Sandbox logs as well?
thoraxe commentedon Aug 18, 2026
@johntmyers Here are the sandbox-side logs, pulled via
GetSandboxLogs(theopenshell-supervisor-networksidecar doesn't write to stdout, sokubectl/oc logson that container is empty — everything goes through the structured/OCSF buffer instead).Fresh repro, same gateway/chart
0.0.104and config as before (topology = "sidecar",process_binary_aware_network_policy = true,policy_validation_failure_mode = "fail_closed"), same 3x identicalExecSandboxattempts against the same undeclared private.svctarget:Sandbox logs (
GetSandboxLogs,sandbox_id=caa43e1b-33f3-4791-a627-9074fca0493b, namessrf-repro-2):Gateway logs, same window (ANSI stripped):
This changes the diagnosis. The sandbox's own OCSF log shows the OPA engine evaluating and logging
DENIEDon all 3 attempts:But the corresponding
ExecSandboxoutput I posted earlier for these exact same 3 attempts got a real403from the target every time — a completed TCP+TLS+HTTP round trip, neverERR/timeout. So this isn't the "approved-rule-never-consulted" or "SSRF-rejection-after-approval" pattern I originally suspected — the policy engine is correctly identifying the connection as disallowed and logging a deny for it, but that decision is not actually being enforced at the network layer on thissidecartopology +process_binary_aware_network_policyconfig. The log and the real network outcome disagree.One more thing worth noting:
GetSandboxLogswas the only way to get this — theopenshell-supervisor-networksidecar container doesn't emit anything to stdout/stderr, so plainkubectl/oc logs -c openshell-supervisor-networkon the sandbox pod returns nothing. If that's expected, it might be worth documenting (or exposing viaoc logstoo) since it's not discoverable otherwise.Happy to pull anything else — pcap from inside the sandbox netns, the OPA policy bundle/decision trace, etc. — if it'd help narrow this down further.
johntmyers commentedon Aug 18, 2026
@thoraxe Thanks for collecting the sandbox logs. The latest script verifies that an undeclared endpoint is denied, but it stops before exercising the approve/reload/retry workflow described in the original issue.
Could you extend the script to perform the complete sequence in one run?
SubmitPolicyAnalysis.GetDraftPolicy.ApproveAllDraftChunks.status=loaded.GetDraftPolicyagain.Please use a unique request path or correlation value for each attempt so the client output, OCSF events, gateway logs, and target logs can be matched unambiguously.
The current
STATUS 403result is consistent with OpenShell enforcing the pre-approval OPA denial: the forward proxy itself returns HTTP 403 with an"error":"policy_denied"JSON body. Printing the body will distinguish that response from a 403 generated by the vLLM service.The complete sequence should tell us:
SubmitPolicyAnalysisreuses the existing chunk ID or creates a new pending row.Without the approval, reload wait, and post-approval retry in the same script, the latest reproduction does not exercise the behavior reported in the original issue.
thoraxe commentedon Aug 19, 2026
Ran the exact sequence you asked for (@johntmyers) — full round trip against a live gateway (v0.0.106,
topology = "sidecar"), via the realCreateSandbox/ExecSandbox/GetDraftPolicy/ApproveAllDraftChunks/GetSandboxPolicyStatusRPCs, mTLS + a Keycloak JWT withaud: openshell-cliand theuserrealm role.Setup:
CreateSandboxwith nospec.policyat all (undeclared endpoint), so the first request is denied outright.1. Attempt #1 (unique correlation value):
curlfrom inside the sandbox to a private, undeclared.svctarget:2.
GetDraftPolicy(after waiting for the automaticSubmitPolicyAnalysis):3.
ApproveAllDraftChunks→policy_version: 2,chunks_approved: 1.4. Polled
GetSandboxPolicyStatusuntilstatus: POLICY_STATUS_LOADED,active_version: 2— confirmed the approved rule is present verbatim inrevision.policy.network_policies.5. Attempt #2 (identical request, new unique correlation value):
Note the error changed from
policy_deniedtossrf_denied— the policy-level check now passes; a separate SSRF check on the private/internal-IP destination is what's rejecting the retry.6.
GetDraftPolicyagain: same chunk id (66d1a2cc-...), status now"approved",draft_versionstill1— no new duplicate draft chunk was created on the retry.This matches your alternative explanation from the first comment exactly: the approved rule is genuinely consulted and enforced (visible directly in the policy revision, and the denial reason changes), and the retry fails at the orthogonal SSRF layer, not because enforcement ignores the approval. It also doesn't reproduce the "brand-new draft chunk" symptom from the original report — same chunk id and
draft_versionboth times.I can't speak to whether this was already true on the
v0.0.104/commitc4b500abuild the original report used, or whether something changed since — but on currentv0.0.106I cannot reproduce "approval doesn't unblock traffic" as originally reported. Given this, and that the SSRF-rejection explanation is now directly confirmed rather than theorized, I think this can be closed. Thanks for the persistence in tracking down the real mechanism here — happy to re-open with a fresh repro if this surfaces again on a non-private target.thoraxe commentedon Aug 19, 2026
Confirmed via live repro above: this doesn't reproduce as reported on v0.0.106. Closing.