feat(net): reverse-proxy auth gate (Bearer / Basic) with X-Forwarded-User#3190
Open
helix-nine wants to merge 3 commits intomasterfrom
Open
feat(net): reverse-proxy auth gate (Bearer / Basic) with X-Forwarded-User#3190helix-nine wants to merge 3 commits intomasterfrom
helix-nine wants to merge 3 commits intomasterfrom
Conversation
dr-bonez
requested changes
Apr 29, 2026
Comment on lines
+245
to
+246
| /// Setting this implies HTTP-aware proxying (same path as | ||
| /// `add_x_forwarded_headers`). |
Member
There was a problem hiding this comment.
make a similar doc comment on add_x_forwarded_headers, and drop the "same path as" parenthetical
Author
|
Done — pushed as
|
dr-bonez
approved these changes
Apr 29, 2026
Author
|
Thanks for the review, @dr-bonez! 🙏 |
added 3 commits
April 29, 2026 06:33
Adds an optional `auth` field to `AddSslOptions` that lets the OS reverse proxy attach an `Authorization` header (Bearer or Basic) to each upstream HTTP request, mirroring how `addXForwardedHeaders` is plumbed today. - core: `ProxyAuth` enum (`bearer` / `basic`) lives next to `AddSslOptions`. `ProxyTarget` carries it through to `run_http_proxy`, which now also takes an explicit `add_forwarded` flag and a pre-rendered `HeaderValue`. A connection is routed through the HTTP-aware path whenever forwarded headers OR auth injection are enabled (was: only forwarded headers). - sdk: TS bindings regenerated. `Host.ts` re-exports `ProxyAuth` and `bindPortForKnown` defaults `auth` to `null` so package devs can override per-binding via the `addSsl` partial. - container-runtime / mock-patch: default new `auth: null` on existing `AddSslOptions` literals. Header values are rendered once per stream (not per request). Encoding errors are logged and the connection falls back to no auth injection rather than failing closed.
…User
Flips the semantics of `ProxyAuth` from "inject Authorization toward
upstream" to "validate Authorization from clients":
- `ProxyAuth::Basic { credentials: Vec<BasicCredential> }` now takes a
list of accepted (username, password) pairs. On a successful match
the proxy forwards the authenticated username to the upstream as
`X-Forwarded-User`.
- `ProxyAuth::Bearer { tokens: Vec<String> }` likewise takes a list of
accepted tokens. Bearer has no user concept, so X-Forwarded-User is
not set on success.
- Unauthenticated / unknown-credential requests get
`401 Unauthorized` with a `WWW-Authenticate: Basic|Bearer realm=...`
challenge. The challenge and the credential lookup map are compiled
once per stream into an `AuthGate`, so per-request cost is one
HashMap probe \u2014 no per-request base64 or per-credential loop.
- Client-supplied `X-Forwarded-User` is always stripped (even on
bindings without a gate, even on bindings without
`add_x_forwarded_headers`) so an upstream service can never be
tricked into trusting an attacker-controlled identity.
- Response body type unified through `http_body_util::BoxBody` so the
service_fn can return either an upstream response or a synthetic 401.
- Adds 5 unit tests covering: accept-list matching, X-Forwarded-User
injection, rejection + WWW-Authenticate challenge, bearer behaviour,
and X-Forwarded-User stripping (with and without gate).
… parenthetical Per review feedback on #3190: parallel doc comment on `add_x_forwarded_headers` describing what it does, and drop the "same path as `add_x_forwarded_headers`" parenthetical from `auth`'s doc since the cross-reference is no longer needed once both fields are self-documenting.
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.
Summary
Adds an optional
authgate onAddSslOptionsso a binding can require Bearer or Basic auth at the OS reverse proxy. Authenticated Basic users are forwarded upstream asX-Forwarded-User, in the same spirit as the existingaddXForwardedHeadersplumbing.Unauthenticated / unknown-credential requests get
401 Unauthorizedwith aWWW-Authenticate: Basic realm="StartOS"(orBearer …) challenge.Design
ProxyAuthincore/src/net/host/binding.rs:Basic { credentials: Vec<BasicCredential> }— list of accepted(username, password)pairs. Matched username is forwarded asX-Forwarded-User.Bearer { tokens: Vec<String> }— list of accepted tokens. No user is forwarded (no user concept).AuthGate(incore/src/net/http.rs): compiled once per stream fromProxyAuth. Stores aHashMap<HeaderValue, Option<HeaderValue>>keyed by the fullAuthorizationvalue ("Basic dXNlcjpwYXNz"/"Bearer foo") and a pre-builtWWW-Authenticateheader value. Per-request cost is oneHeaderMap::get+ oneHashMapprobe — no per-request base64 / no per-credential loop.apply_request_policy: single function called from both the HTTP/1 and HTTP/2 service_fn. It (a) strips client-suppliedX-Forwarded-*headers, (b) runs the gate, (c) setsX-Forwarded-Proto/X-Forwarded-Forifadd_forwarded, (d) setsX-Forwarded-Useron a successful Basic match.Response<BoxBody<Bytes, …>>so we can return either an upstreamhyper::body::Incomingor a syntheticFull<Bytes>401 body from the same closure.Security notes
X-Forwarded-Useris always stripped, even on bindings without a gate and even whenaddXForwardedHeadersisfalse. An upstream service that trusts that header can never be tricked by a malicious client.HashMap(not constant-time). These are operator-controlled accept-lists, not user-supplied passwords being verified against a database, so timing leakage of the credential set size is not in the threat model. If we want constant-time later we can swap the inner check.HeaderValue) are logged and the gate is dropped, leaving the binding behaving as ifauthwere unset rather than 401-ing every request — i.e. configuration errors never make a binding more permissive than the operator intended.What changed
core/src/net/host/binding.rs:ProxyAuthenum +BasicCredentialstruct, both#[ts(export)].core/src/net/http.rs:AuthGate,apply_request_policy,BoxBody-based response type, refactored HTTP/1 + HTTP/2 service_fn, 5 unit tests.core/src/net/vhost.rs: compilesAuthGateonce per stream and routes throughrun_http_proxywhenever forwarded-headers OR auth-gate is set.core/src/net/net_controller.rs,core/src/db/model/public.rs: defaultauth: NoneonAddSslOptionsliterals.sdk/base/lib/osBindings/: regenerated bindings (newProxyAuth.ts, newBasicCredential.ts, updatedAddSslOptions.ts).sdk/base/lib/interfaces/Host.ts: re-exportsProxyAuthandBasicCredential.bindPortForKnowndefaultsauth: nullso package devs override per-binding via theaddSslpartial.container-runtime/,web/projects/ui/src/app/services/api/mock-patch.ts: defaultauth: nullon existing literals.Verification
cargo check -p start-os --lib✅cargo test --lib --features test net::(incl. allexport_bindings_*and the 5 new gate tests) ✅cd sdk/base && npm run tsc && npm test✅cd sdk/package && npm run tsc✅cd container-runtime && npx tsc --noEmit✅Notes
run_http2_proxyis called withadd_forwarded=false, gate=None— the outer hop already validated the request, and the inner call only carries the upgraded byte stream.