fix(proxy): don't cross-deny sibling routes sharing an upstream - #1437
Merged
SequeI merged 1 commit intoJul 17, 2026
Merged
Conversation
select_intercept_route returned 403 as soon as any route for the upstream evaluated to Deny. A legacy endpoint_rules allow-list compiles to a non-explicit default-deny policy, so when several credential routes share one upstream host with disjoint endpoint_rules, the first route (by prefix order) whose rules didn't match the request path hard-denied the whole request, even though a sibling route would have authorized and injected a credential. Treat a non-explicit default-deny as "this route does not apply" instead of a terminal 403, mirroring route::select_route: drop a managed-credential route, or let a credential-less _ep_ route gate the request via has_endpoint_only_route so the post-loop check still 403s only when nothing authorized it. Explicit endpoint policies keep their authoritative hard-deny. Fix covers both the HTTP/1.1 and HTTP/2 intercept paths. Add a regression test for two disjoint credential routes on one upstream. Signed-off-by: Zalan Blenessy <zalan.blenessy@volvocars.com>
Contributor
PR Review SummarySize
Affected crates
Blast radius — ContainedThis PR touches: source code Updated automatically on each push to this PR. |
SequeI
approved these changes
Jul 17, 2026
SequeI
left a comment
Member
There was a problem hiding this comment.
Makes sense and LGTM, thanks for the fix!
christine-at-datadog
added a commit
to christine-at-datadog/nono
that referenced
this pull request
Jul 18, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
kipz
pushed a commit
to kipz/nono
that referenced
this pull request
Jul 24, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
kipz
pushed a commit
to kipz/nono
that referenced
this pull request
Aug 3, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
1 task
kipz
pushed a commit
to kipz/nono
that referenced
this pull request
Aug 3, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
kipz
pushed a commit
to kipz/nono
that referenced
this pull request
Aug 6, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
kipz
pushed a commit
to kipz/nono
that referenced
this pull request
Aug 9, 2026
main gained a disjoint-credential-routes regression test (nolabs-ai#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com>
lukehinds
added a commit
that referenced
this pull request
Aug 10, 2026
…ercept routes (#1443) * feat(proxy): add authenticated WebSocket tunneling for CONNECT/TLS-intercept routes Codex-style credential-provider routes hang for ~1min on WebSocket upgrades because they route through the CONNECT/TLS-intercept path, not the reverse-proxy path Part 1 covered. Adds fail-fast upgrade classification, a declarative per-route upgrade allow-list (UpgradeProtocol/UpgradeRuleConfig, compiled into LoadedRoute and CredentialRouteDef), and handle_websocket_upgrade in tls_intercept/handle.rs, which reuses the existing header filtering, credential injection, and phantom-nonce resolution pipeline before dialing the upstream raw and relaying bytes via copy_bidirectional after a validated 101 handshake. Signed-off-by: Christine Le <christine.le@datadoghq.com> * feat(proxy): route-integrated WebSocket upgrade gating and strict HTTP/1 framing Move WebSocket upgrade-rule matching into route selection itself so an unmatched upgrade path is rejected before any upstream is contacted, add an explicit 501 for SPIFFE/AWS-authenticated routes (WebSocket tunneling is unsupported for those credential mechanisms), and make phantom-nonce resolution fail closed: an unresolved phantom nonce in a forwarded header now yields a 403 instead of leaking the unresolved token upstream. Simplify the declarative upgrade-rule schema to just a path (protocol and method are implied for classic WebSocket), and split handshake parsing and strict, framing-aware response relaying into new http1.rs/websocket.rs modules shared between the WS and non-WS intercept paths. Signed-off-by: Christine Le <christine.le@datadoghq.com> * fix(proxy): update route-selection test for rebase onto main main gained a disjoint-credential-routes regression test (#1437) written against the pre-refactor select_intercept_route signature. Update it to use InterceptRouteRequest/SelectedRoute so it compiles after the rebase. Signed-off-by: Christine Le <christine.le@datadoghq.com> * fix(proxy): add missing upgrades field to Linux-only server.rs tests Two RouteConfig literals in tests gated by #[cfg(not(target_os = "macos"))] were missing the new upgrades field, so they only failed to compile on Linux CI (invisible on a macOS dev machine). Signed-off-by: Christine Le <christine.le@datadoghq.com> * fix(proxy): address PR #1443 review findings for WS tunnel intercept Forward client bytes buffered immediately after the WS upgrade headers to upstream instead of silently dropping them, verify Sec-WebSocket-Accept against RFC 6455, reject h2 extended-CONNECT WebSocket upgrades that would otherwise skip upgrade_rules, validate resolved nonce values for CRLF/NUL before header injection, collapse dot-segments in normalize_path, and collapse the duplicate ad-hoc header parser onto the shared strict parser. Signed-off-by: Christine Le <christine.le@datadoghq.com> * fix(cli): add upgrades to credential_routes JSON schema kipz reported that profiles using the new upgrades field on credential_routes parse and pass `nono profile validate --strict` but fail validation against the published schema, since CredentialRouteDef there still only lists [base_url_env_var, endpoint_policy, env_var, name, provider]. Add upgrades (CredentialWebSocketRuleDef: origin, path) to match the Rust model, plus a schema_shape regression test. * fix(cli): tighten upgrades origin/path schema to match runtime validation The prior commit's CredentialWebSocketRuleDef used bare unconstrained strings for origin/path, looser than both the sibling CredentialProviderTokenEndpoint schema (host/path) and the actual runtime checks in validate_provider_origin/validate_provider_path, which require an https-only origin with no path/query/fragment and an absolute path starting with '/'. Use the existing UrlOrigin $ref for origin and add minLength/pattern for path so malformed profiles fail schema validation instead of only failing later at `nono profile validate --strict`. * fix(proxy): update select_intercept_route tests to current signature The approval-flow tests in tls_intercept::handle were written against a pre-merge version of select_intercept_route/SelectedRoute/RouteConfig and no longer compiled after merging main: the function now takes an InterceptRouteRequest struct instead of positional method/path args, SelectedRoute needs Debug for panic! formatting, and RouteConfig gained a required `upgrades` field. Signed-off-by: christine.le <christine.le@datadoghq.com> --------- Signed-off-by: Christine Le <christine.le@datadoghq.com> Signed-off-by: christine.le <christine.le@datadoghq.com> Co-authored-by: Luke Hinds <lukehinds@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
select_intercept_route returned 403 as soon as any route for the upstream evaluated to Deny. A legacy endpoint_rules allow-list compiles to a non-explicit default-deny policy, so when several credential routes share one upstream host with disjoint endpoint_rules, the first route (by prefix order) whose rules didn't match the request path hard-denied the whole request, even though a sibling route would have authorized and injected a credential.
Treat a non-explicit default-deny as "this route does not apply" instead of a terminal 403, mirroring route::select_route: drop a managed-credential route, or let a credential-less ep route gate the request via has_endpoint_only_route so the post-loop check still 403s only when nothing authorized it. Explicit endpoint policies keep their authoritative hard-deny. Fix covers both the HTTP/1.1 and HTTP/2 intercept paths.
Add a regression test for two disjoint credential routes on one upstream.
Linked Issue
Closes #1436
Summary
Fixes #1436
Test Plan
Checklist
CHANGELOG.mdif needed