fix(server): gate only newly authored uninspected credentialed endpoints - #3129
fix(server): gate only newly authored uninspected credentialed endpoints#3129feloy wants to merge 3 commits into
Conversation
Attaching a credentialed provider made every later `policy set` and `policy update` fail, even for unrelated edits: credentialed endpoint 'github.com:443' in rule 'pypi' uses L4-only; configure L7 inspection or explicitly set allow_uninspected_credentials: true The rule named in the error is one the operator never wrote. A sandbox image's baked policy reaches the store through the supervisor sync path, which skips the credential gate by design — rejecting that delivery would crash-loop the sandbox. Sandbox create passes too, because it only sees the request's policy plus the provider layers. Endpoint stamping then marks any endpoint overlapping a credentialed provider's host and port, so the image's L4-only `github.com:443` becomes credentialed, and every operator edit was re-validated against the whole effective policy and rejected. The operator could not even edit the offending endpoint, because that edit was rejected as well. Gate the difference on edit paths instead. An edit is rejected for the uninspected credentialed endpoints it introduces; endpoints the current policy already carries are logged and admitted. Endpoint identity is the rule name, host, port, and uninspected mode, so moving an inherited endpoint from L4-only to `tls: skip` is a fresh authoring act. Stamping is untouched and stays a full recomputation. Sandbox create and provider attach keep the strict whole-policy gate, since attaching a credentialed provider is a new credential exposure. Nothing is granted by admitting an inherited endpoint: the network supervisor still denies that traffic. Refs: NVIDIA#2998 Signed-off-by: Philippe Martin <phmartin@redhat.com>
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
This is valid, focused work for the second acceptance criterion in #2998, and the docs cover the changed policy behavior. The initial review found two reachable correctness gaps in the new differential gate.
Action required: @feloy, please preserve the complete effective port set in inherited endpoint identity and make full-policy replacement validation use the policy revision it commits against.
Blocking findings:
GATOR-b4f81f7e-01: multi-port expansion can inherit a lossy first-port identityGATOR-b4f81f7e-02: concurrent full replacement can use a stale inherited baseline
Carried findings:
- None
Non-blocking suggestions:
- None
Gator metadata
- Validation: Project-valid because it directly implements the policy round-trip acceptance criterion in validated issue #2998.
- Docs: Fern policy docs and the security architecture document are updated.
- Checks: DCO passes; contributor workflows have not been dispatched because copy-PR validation is still pending.
- E2E:
test:e2eis required for policy-enforcement behavior, but dispatch waits until blocking review feedback is resolved. - Head SHA:
b4f81f7e5cfaf3272a0a7c84bda5e4b513e1118a - Base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Merge base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Patch ID:
3053d1de810baad7df2a0fb1b31f477678cb5651 - Gator payload:
8 - Review mode:
initial - Previous reviewed SHA: none
- Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:in-review
…e on commit Review of the differential credential gate found two reachable gaps. An uninspected credentialed endpoint's identity collapsed a multi-port endpoint onto its first port. Keeping an inherited port and adding a second one to the same endpoint therefore matched the baseline and was admitted, even though the added port had never been classified. Identity is now one key per effective port, so widening `[443]` to `[443, 8443]` introduces `8443`, while reordering or narrowing the port set carries the endpoint forward. Full policy replacement classified the edit before its persistence loop. Between that check and the write, another writer could remove the violation the edit was inheriting; the replacement then restored it without ever being classified. The gate now runs inside each persistence attempt, against the revision that attempt commits against. The write claims `latest.version + 1`, so any revision landing after that read loses the unique-version race and the retry reclassifies against the newer policy. Refs: NVIDIA#2998 Signed-off-by: Philippe Martin <phmartin@redhat.com>
|
Label |
|
/ok to test 198906a |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @feloy. I checked the per-port identity change and the relocation of replacement validation into the versioned persistence attempt on head 198906ab9c0cdd76ca3b4b2e97f2fac952c56066; both prior findings are resolved, their Gator threads are closed, and the bounded follow-up review found no new blockers. The required E2E dispatch has been authorized and is waiting for the copy-PR mirror before Branch E2E can be rerun.
Blocking findings:
- No blocking findings remain
Carried findings:
- None
Gator metadata
- Validation: Project-valid because it directly implements the policy round-trip acceptance criterion in validated issue #2998.
- Docs: Fern policy docs and the security architecture document are updated.
- Checks: DCO passes; contributor workflows for this head are not yet dispatched because the copy-PR mirror is still being created.
- E2E:
test:e2eis applied and/ok to test 198906ab9c0cdd76ca3b4b2e97f2fac952c56066was posted; Branch E2E is not yet queued. - Head SHA:
198906ab9c0cdd76ca3b4b2e97f2fac952c56066 - Base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Merge base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Patch ID:
d963565afa6ca996167ba81fd15c675e248310b8 - Gator payload:
8 - Review mode:
follow_up - Previous reviewed SHA:
b4f81f7e5cfaf3272a0a7c84bda5e4b513e1118a - Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:in-review
|
Label |
Clippy's unnecessary_unwrap fires on unwrap_err() after an is_ok() check, failing the workspace lint under -D warnings. Bind the status directly with if let Err instead. Signed-off-by: Philippe Martin <phmartin@redhat.com>
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
The bounded follow-up review checked the clippy-only assertion rewrite on head 4d3c37c476d3223af65864b3e9584387f1558b21. It preserves the test's two outcomes, introduces no new blocker, and does not reopen either resolved finding.
Blocking findings:
- No blocking findings remain
Carried findings:
- None
Gator metadata
- Validation: Project-valid because it directly implements the policy round-trip acceptance criterion in validated issue #2998.
- Docs: Existing Fern policy docs and the security architecture update remain sufficient; this delta has no UX change.
- Checks: DCO passes; Branch Checks, Helm Lint, and E2E are waiting for the current-head copy-PR mirror.
- E2E:
test:e2eremains required and applied; current-head dispatch is pending/ok to testmirror refresh. - Head SHA:
4d3c37c476d3223af65864b3e9584387f1558b21 - Base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Merge base SHA:
8bc79552638a625971f8c6d30c26efa917a19cf9 - Patch ID:
891cab22d4b514b493be7a213b8cef92ed25864f - Gator payload:
8 - Review mode:
follow_up - Previous reviewed SHA:
198906ab9c0cdd76ca3b4b2e97f2fac952c56066 - Review budget exhausted: no
- Maintainer decision required: no
- Next state:
gator:in-review
|
/ok to test 4d3c37c |
|
Label |
|
I want to hold on this PR. I think there's a deeper rooted set of issues to address which I am putting together an outline for. |
Summary
Attaching a credentialed provider to a sandbox made every later
openshell policy setandopenshell policy updatefail, even for completely unrelated edits, naming a rule the operator never authored. The credential gate now rejects only the uninspected credentialed endpoints an edit introduces, while endpoints the sandbox's current policy already carries are logged and admitted.Related Issue
Refs #2998 (second acceptance criterion). Reported downstream as openkaiden/kaiden#2759.
Note for maintainers: #2998 is
state:validatedbut is notstate:acceptedand is not on the roadmap, so no human acceptance signal has been recorded yet. This PR was opened on a direct contributor request; no lifecycle or workflow labels were changed.Changes
The bug. Operators saw this on edits that had nothing to do with the endpoint named:
Reported variants named
pypi,copilot, andclaude_code— all rules that come from the sandbox image's baked default policy, not from the operator.Root cause. Three things combine:
docs/reference/default-policy.mdx). The tutorial reproduces it atdocs/get-started/tutorials/github-sandbox.mdx: thepypirule allowsgithub.com:443andapi.github.com:443with noprotocol, i.e. L4-only.grpc/policy.rs, sandbox-caller branch ofhandle_update_config_inner).sandbox create --provider …passes too, because it only validates the request's policy plus the provider layers, both of which are clean.stamp_provider_credentialed_endpointsconservatively marks any endpoint whose host and port overlap a credentialed provider's scope. The image's L4-onlygithub.com:443in rulepypitherefore becomes credentialed.Every operator-authored edit then re-validated the whole effective policy and rejected on that inherited endpoint. The gateway admitted a policy through one door and refused to let the operator touch it through any other — including the edit that would have fixed the offending endpoint.
The fix. The gate is now differential on edit paths and strict on authoring paths:
collect_uninspected_credentialed_endpointsreplaces the first-matchfind_…, andUninspectedCredentialedEndpointKey(rule name, host, port, uninspected mode) gives each violation an identity.modeparticipates deliberately: moving an inherited endpoint from L4-only totls: skipis a fresh authoring act, not the same finding carried forward.validate_uninspected_credentialed_endpoints_excluding(policy, inherited)rejects only violations absent from the baseline; inherited ones emit awarn!and pass. The reported endpoint is chosen deterministically, so the message no longer depends on map iteration order.stamp_operator_merged_credential_policyand remains a full recomputation. Only the accept/reject decision is differential — the invariant inarchitecture/security-policy.md("a delta-based derivation would let a series of individually valid edits reach a state no single edit would have admitted") still holds.current_policyinstage_validated_merge_operationandapply_merge_operations_with_retry,base_policyin proposal evaluation, and a newvalidate_updated_sandbox_credential_policyforpolicy set(built on the existingcurrent_base_policy_for_sandbox+effective_policy_for_source, so baseline and candidate are composed and stamped by the identical routine).sandbox createand provider attach keep the strict whole-policy gate: attaching a credentialed provider is a genuinely new credential exposure and must fail loudly.Why this is safe. The admission gate is not the security boundary.
architecture/security-policy.mdalready states the network supervisor independently enforces the same boundary, and the delivery path already declines to fail closed for exactly this reason ("the runtime backstop denies the traffic instead"). Admitting an inherited endpoint grants nothing — the proxy still denies that traffic until the endpoint gets an inspected protocol or the explicitallow_uninspected_credentialsexception.Alternatives considered and rejected: narrowing the stamp to the provider's own rule (the conservative host+port scope is deliberate per
architecture/security-policy.md, and narrowing it weakens the control); gating the supervisor sync path (crash-loops sandboxes running stock images); a blanket--allow-uninspected-credentialsCLI flag (silently rewrites the operator's policy to disable a security control).Docs:
architecture/security-policy.mdgains the differential-gate rule and its rationale;docs/sandboxes/policies.mdxnotes that an inherited uninspected credentialed endpoint does not block unrelated edits and that its traffic is still denied at runtime.Testing
Verified against this implementation on this branch:
cargo test -p openshell-server— 1459 tests pass, 0 failures.mise run pre-commit— passes (lint, format, license headers).mise run test— full suite.Six new tests in
crates/openshell-server/src/grpc/policy.rs, covering both the bug and the boundary that must not move:policy update --add-endpointpolicy setreplaying the current base policy (thepolicy get --baseround trip)policy setFailedPrecondition, message names the new ruletls: skipThe integration fixture mirrors the real setup: a stored profile shaped like
providers/github.yaml, a policy carrying the tutorial's L4-onlypypirule, delivered through the ungated sandbox-sync path exactly as an image policy arrives in production.One note for reviewers:
merge_policyfolds an added rule into whichever existing rule already owns that host and port (crates/openshell-policy/src/merge.rs). The first draft of the rejection test passed for the wrong reason because of this — the new rule was absorbed intopypirather than added. The test now targets a host no existing rule covers, with the reason recorded inline.mise run pre-commitpassese2e/rust/tests/credential_gating.rsalready covers the strict gate end to end; the new behavior is fully exercised by unit tests against the real gRPC handlers, including the store-backed merge-retry loop.Checklist
Out of scope
Deliberately left for separate work, called out so they are not lost:
Errorphase on exit and cannot be reconnected). Depends on open feature issue feat(sandbox): add main restart policy #2798 (feat(sandbox): add main restart policy).openshell policy get --baseprints a metadata header above the---line that must be hand-stripped beforepolicy setcan consume the file.docs/sandboxes/policies.mdxdocuments the manual step. A-o yamloutput emitting only the policy body would close the round trip.providers/pypi.yamlships L4-onlygithub.com:443andapi.github.com:443. Attachingpypiand a credentialed GitHub provider to the same sandbox still fails at create time, on the strict path this PR intentionally leaves intact. That is a real bug in its own right and deserves its own issue.