Skip to content

Send requests to the destination and credential snyk-broker would - #128

Closed
shawnburke wants to merge 1 commit into
mainfrom
grpc/accept-rules-audit
Closed

Send requests to the destination and credential snyk-broker would#128
shawnburke wants to merge 1 commit into
mainfrom
grpc/accept-rules-audit

Conversation

@shawnburke

@shawnburke shawnburke commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Context

This is a stack of PRs to get the new gRPC tunnel up to spec with snyk-broker --- with no snyk-broker in the picture we have to implement all of the accept file parsing and routing in Go.

Specifically I had Claude audit all of the test cases in snyk-broker, figure out which ones we care about, and make sure that the new tunnel supports them properly. In addition, this folds in the new wildcards support.

This PR is the culmination of that and is about 400 lines of code changes and then some things moving around and a lot of tests.

But this is the meat of the change, the earlier PRs were to ensure we parse accept files properly, this one focuses that we then apply and route them.

It adds and validates the following cases in the grpc path and does not touch the existing snyk-broker flows:

  • one wildcard, leftmost label only, https only, registrable domain required
  • a family requires a target host; absent, duplicated or outside-policy is refused
  • the port comes from the origin, never the header
  • a websocket upgrade on a family is refused
  • a concrete origin refuses a target host that disagrees with it
  • the header comes off the request before rules are consulted, so it can neither reach the upstream nor select a rule

Claude stuff

Stack 4/4 — the last one. 1/4, 2/4 and 3/4 have all merged, so this now sits directly on main as a single commit.

The last of the parity work: where a matched rule actually sends the request, and what credential rides along.

Wildcard origins work on the tunnel path

Route previously parsed https://*.googleapis.com, found a scheme and a host, and returned it verbatim — so accept.google.json dialed a literal *.googleapis.com. X-Cortex-Target-Host was neither validated nor removed, but forwarded to the upstream.

The Router now enforces the policy the reflector does:

  • one wildcard, leftmost label only, https only, registrable domain required
  • a family requires a target host; absent, duplicated or outside-policy is refused
  • the port comes from the origin, never the header
  • a websocket upgrade on a family is refused
  • a concrete origin refuses a target host that disagrees with it
  • the header comes off the request before rules are consulted, so it can neither reach the upstream nor select a rule

Written fresh in acceptfile rather than lifted from the reflector, which keeps its own copy. origin_test.go mirrors the reflector's wildcard suite case for case — that's how we know they agree while both exist.

One thing stops the agent: a malformed wildcard origin. Not a migration risk — the snyk-broker path refuses it too, at render or by panicking when the reflector is off, so no working deployment carries one. And warn-and-ignore has no safe meaning there: treating a bad family as permissive would authorize hosts nobody chose. Every other origin problem warns and fails per request, as it does on the snyk-broker path.

Credentials match auth-header.ts

token emitted Bearer, raw emitted raw instead of the bare token, and basic with a pre-encoded token dropped it and sent Basic Og== — base64 of ":", an empty credential.

A scheme is now supported exactly when authHeaderBuilders has a builder for it, so the set and the behaviour can't drift — they were previously a set in one file and a switch in another, raw literals in both. An unrecognized scheme warns and sends no header, which is what the broker does with one.

Pools rotate again

Origin() expanded ${VAR} before the pool was consulted, so a variable that exists only as VAR_POOL became the empty string and every request failed — while pool_test.go, which exercises PoolManager directly, stayed green. varIsSet accepts VAR_POOL as satisfying a required variable, so such a file loaded fine and then failed at runtime.

Structure

auth.go, headers.go and origin.go are new files rather than more of router.go — auth, header handling and origin policy each pulled out whole. parseOrigin returns a parsedOrigin{url, wildcard} so the two halves of a destination decision travel together, and resolveTargetHost is a method on it.

What the snyk-broker path sees

The only non-test consumers of acceptfile outside grpctunnel are common/integration.go (NewAcceptFile, AcceptFile), reflector.go (ResolverMap, StringValueResolver) and relay_instance_manager.go (RenderContext, route.Origin()). Nothing in origin.go, auth.go, headers.go, matcher.go or router.go is reachable from there — Path() and MatchRule have no callers outside acceptfile and grpctunnel.

That leaves two genuinely shared files, and both preserve behaviour:

  • ResolvePoolVars now delegates to resolveVars(s, true), which is its old body verbatim. Peek is new and only tunnel startup validation calls it.
  • Origin() is the one real shared accessor. TestOriginContract (landed in 2/4) pins it, and it was also checked differentially against main's exact body across 17 origin shapes — scheme-less *.googleapis.com, 1.2.3.4:8080, //protocol-relative, [::1]:8443, ftp://, %2F mid-path — identical in all 17, the pre-existing panic on a scheme-less IP:port included.

REFLECTOR_TESTS=1 go test ./server/snykbroker/... ./common/... is green.

No existing test or fixture is modified: the diff against main for agent/common/, agent/test/relay/, agent/test/load/, reflector.go and relay_instance_manager.go is empty.

Known remaining divergences

Both are more permissive than snyk-broker, so a request the broker routed still routes:

  • valid header values compare case-insensitively; the broker is case-sensitive.
  • An empty values array means "the header must be present"; the broker rejects everything.

And one open question, not decided here: a caller-supplied Authorization still passes through when the matched rule declares no auth. Pre-existing behaviour this stack doesn't change.


The stack

PR
1 #129 flake fixes — merged
2 #130 warn-and-ignore, + unbroke mainmerged
3 #131 matching parity — merged
4 this one wildcard origins, auth, pools

🤖 Generated with Claude Code

https://claude.ai/code/session_01DFYsRuHoCbCBWEFvuaj9kV

@shawnburke shawnburke changed the title Make the tunnel Router the authoritative accept-file engine Ensure GRPC-router properly handles snyk-broker config semantics Aug 26, 2026
@shawnburke shawnburke changed the title Ensure GRPC-router properly handles snyk-broker config semantics Harden the tunnel Router so switching from snyk-broker is seamless Aug 26, 2026
@shawnburke
shawnburke marked this pull request as draft August 26, 2026 22:01
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from d48277c to 458203f Compare August 27, 2026 00:08
@shawnburke shawnburke changed the title Harden the tunnel Router so switching from snyk-broker is seamless Send requests to the destination and credential snyk-broker would Aug 27, 2026
@shawnburke
shawnburke changed the base branch from main to grpc/accept-3-matching August 27, 2026 00:09
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from 458203f to 3fb4b44 Compare August 27, 2026 00:20
@shawnburke
shawnburke marked this pull request as ready for review August 27, 2026 00:26
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from 3fb4b44 to 5464337 Compare August 27, 2026 00:32
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from 5464337 to d2bc9af Compare August 27, 2026 01:47
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from d2bc9af to 339ec8c Compare August 27, 2026 06:12
@shawnburke
shawnburke force-pushed the grpc/accept-rules-audit branch from 339ec8c to f8b0145 Compare August 27, 2026 21:03
Base automatically changed from grpc/accept-3-matching to main August 27, 2026 21:16
The last of the parity work: where a matched rule actually sends the
request, and what credential rides along.

Wildcard origins work on the tunnel path. Route previously parsed
"https://*.googleapis.com", found a scheme and a host, and returned it
verbatim — so accept.google.json dialed a literal "*.googleapis.com", and
X-Cortex-Target-Host was neither validated nor removed but forwarded to
the upstream. The Router now enforces the policy the reflector does: one
wildcard, leftmost label only, https only, a registrable domain required;
the target host required for a family and refused outside it; the port
taken from the origin, never the header; duplicates and websocket upgrades
refused; a concrete origin refusing a host that disagrees with it. The
header comes off the request before rules are consulted, so it can neither
reach the upstream nor select a rule. Written fresh here rather than
lifted from the reflector, which keeps its own copy — the two suites are
how we know they agree while both exist.

A malformed wildcard origin is the one thing that stops the agent, and it
is not a migration risk: the snyk-broker path refuses it too, at render or
by panicking when the reflector is off, so no working deployment carries
one, and treating a bad family as permissive would authorize hosts nobody
chose. Every other origin problem warns and fails per request, the way it
does on the snyk-broker path.

Auth matches auth-header.ts. "token" emitted Bearer, "raw" emitted the
token with a "raw " prefix instead of bare, and basic with a pre-encoded
token dropped it and sent base64(":") — an empty credential. A scheme is
now supported exactly when authHeaderBuilders has a builder for it, so the
set and the behaviour cannot drift; an unrecognized one warns and sends no
header, which is what the broker does with it.

Pool origins rotate again. Origin() expanded ${VAR} before the pool was
consulted, so a variable that exists only as VAR_POOL became the empty
string and every request failed, while pool_test.go — which exercises
PoolManager directly — stayed green.

auth.go, headers.go and origin.go are new files rather than more of
router.go: auth, header handling and origin policy each pulled out whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFYsRuHoCbCBWEFvuaj9kV
@shawnburke

Copy link
Copy Markdown
Collaborator Author

Split into a three-PR stack so the auth fix and the pool fix can be reviewed and merged independently of the wildcard work:

PR base
1 #133 — build the credential snyk-broker builds main
2 #134 — make origin pools rotate again #133
3 #135 — send requests only to an authorized destination #134

#135's tree is byte-identical to this branch, so nothing is lost in the split. #133 is independent of the other two and can go in first.

Branch grpc/accept-rules-audit is left in place for reference.

@shawnburke shawnburke closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants