Repository navigation
sandboxd: per-claim credential injection (#301), streamed egress responses, TLS resumption, egress in the cluster digest - #302
Merged
Conversation
A guest: false env entry may name hosts and a header; the egress proxy sets its value there on intercepted requests only. Every host must be intercepted by the claim's pool and allowed by its tenant class, and may not take a header the pool rule's own secret sets, or the claim or env write answers 400. The proxy reads a per-sandbox env snapshot published on each env write instead of taking the manager lock per request.
relay copied the upstream body with io.Copy and never flushed, so an SSE or chunked response, headers included, stayed in the response buffer until it filled or the origin ended the stream. Like httputil.ReverseProxy, a text/event-stream or unsized response is now flushed after each write, headers first, on both the forward and the intercepted path.
serveIntercept built a fresh tls.Config per CONNECT, each with its own auto ticket keys, so no guest session ever resumed and every new connection paid a full handshake. One ticket key per proxy lets a caching client resume: connect plus one request went from 1.195 ms to 0.962 ms in an interleaved A/B (6 runs each, order swapped); clients without a session cache are unchanged.
Egress classes, pool egress, secret names, headers and value envs, internal allow and the upstream allow list must match across nodes, or a redirected claim reaches different hosts and its credentials are refused or read from another env there; the digest left them out, so drift was silent. They now feed the digest in an order-free form without secret values, and the manager computes it from the config as last reloaded and republishes it after every reload.
This was referenced Oct 2, 2026
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.
Closes #301.
A control plane can now bind each sandbox's own credential to an upstream host per claim. The node config stays the envelope; the claim fills it.
Claim credentials (#301)
A
guest: falseenv entry may carryinject:{"env": {"API_TOKEN": {"value": "Bearer …", "guest": false, "inject": {"hosts": ["api.example.com", "*.git.example.com"], "header": "Authorization"}}}}It rides the claim request and the existing
/envverbs. There is no new resource: rotation is the samePATCH, removal isnull,GETblanks the value and shows the hosts, the entry persists with the claim, forks don't inherit it, and the env limits apply.Envelope. The proxy sets the header only on intercepted (MITM) requests to those hosts, never on a plaintext forward request. Every host must be covered by an
intercept: truerule of the claim's pool and allowed by its tenant class, and a header the pool rule's own secret sets on that host is refused. A violation answers 400 and stores nothing; at claim time the VM is released. Env writes only re-check inject entries that are new or changed, so a later narrowing does not block unrelated env edits.Runtime. The pool secret is set first and wins. Among claim credentials, the first by entry name wins per header. An empty value injects nothing. A reload that drops the covering intercept stops the injection on the next request and logs the affected claims. The audit records
claim:<entry>next to the pool secret's name, never a value.Hot path.
Sandbox.SetEnvpublishes an immutable env snapshot that the proxy reads without the manager lock.m.mu. BothclaimSecrets.HeaderandclaimRouter.Routenow read the snapshot.Credentials()for a claim without injects costs 2 ns and 0 allocs; with one inject, 28 ns and 1 alloc. Claim, wake and fork pay nothing extra.Egress fixes and performance
relaycopied upstream bodies withio.Copyand never flushed, so an SSE or chunked response stayed in the buffer, headers included, until it filled or the origin ended. A scratch probe showed headers and the first event arriving only when the origin closed, 2 s later.text/event-streamand unsized bodies are now flushed per write, headers first, on both the forward and the intercepted path, matchinghttputil.ReverseProxy.serveInterceptbuilt a freshtls.Configper CONNECT. Each had its own auto-generated ticket keys, so no guest session ever resumed: 0 of 1,000 in the benchmark.Cluster digest covers egress
Egress classes, pool egress, secret names, headers and
value_env,egress_internal_allowandegress_upstreamnow feed the gossiped digest, in an order-free form and without values, so drift shows insandboxd_config_digest_mismatch. The digest is computed from the config as last reloaded and is republished after every reload; before this change it used the boot config. Mixed versions report a mismatch during the rollout, since no backward compatibility is kept.Numbers
The prod additions are the new feature: env inject, coverage checks, the snapshot, flush, tickets and the digest. Comment lines are +27/−6, all one-line godocs or WHYs.
Gates
GOWORK=off make go-lintprinted 10×0 issues.on darwin and on linux.make go-testpassed with the PostgreSQL suite enabled, andgo test -racepassed in a non-root linux/arm64 container for sandboxd, e2e and sdk/go.scripts/credentials-e2e.shleg passed: each claim's credential reached postman-echo, the plain claim kept the guest's placeholder, and the pool secret was present./proc/*/environ,/runand/etc, and was blanked inGET /env.