Skip to content

feat(net): reverse-proxy auth gate (Bearer / Basic) with X-Forwarded-User#3190

Open
helix-nine wants to merge 3 commits intomasterfrom
feat/proxy-auth-headers
Open

feat(net): reverse-proxy auth gate (Bearer / Basic) with X-Forwarded-User#3190
helix-nine wants to merge 3 commits intomasterfrom
feat/proxy-auth-headers

Conversation

@helix-nine
Copy link
Copy Markdown

@helix-nine helix-nine commented Apr 28, 2026

Summary

Adds an optional auth gate on AddSslOptions so a binding can require Bearer or Basic auth at the OS reverse proxy. Authenticated Basic users are forwarded upstream as X-Forwarded-User, in the same spirit as the existing addXForwardedHeaders plumbing.

await multi.bindPort(8080, {
  protocol: 'http',
  addSsl: {
    auth: {
      type: 'basic',
      credentials: [
        { username: 'alice', password: 'hunter2' },
        { username: 'bob',   password: 'swordfish' },
      ],
    },
  },
})
// upstream sees:  Authorization: Basic ...   AND   X-Forwarded-User: alice

Unauthenticated / unknown-credential requests get 401 Unauthorized with a WWW-Authenticate: Basic realm="StartOS" (or Bearer …) challenge.

Design

  • ProxyAuth in core/src/net/host/binding.rs:
    • Basic { credentials: Vec<BasicCredential> } — list of accepted (username, password) pairs. Matched username is forwarded as X-Forwarded-User.
    • Bearer { tokens: Vec<String> } — list of accepted tokens. No user is forwarded (no user concept).
  • AuthGate (in core/src/net/http.rs): compiled once per stream from ProxyAuth. Stores a HashMap<HeaderValue, Option<HeaderValue>> keyed by the full Authorization value ("Basic dXNlcjpwYXNz" / "Bearer foo") and a pre-built WWW-Authenticate header value. Per-request cost is one HeaderMap::get + one HashMap probe — 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-supplied X-Forwarded-* headers, (b) runs the gate, (c) sets X-Forwarded-Proto/X-Forwarded-For if add_forwarded, (d) sets X-Forwarded-User on a successful Basic match.
  • Response unification: service_fn returns Response<BoxBody<Bytes, …>> so we can return either an upstream hyper::body::Incoming or a synthetic Full<Bytes> 401 body from the same closure.

Security notes

  • Client-supplied X-Forwarded-User is always stripped, even on bindings without a gate and even when addXForwardedHeaders is false. An upstream service that trusts that header can never be tricked by a malicious client.
  • Lookup is plain 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.
  • Compile errors (e.g. a credential containing chars that don't fit in an HeaderValue) are logged and the gate is dropped, leaving the binding behaving as if auth were 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: ProxyAuth enum + BasicCredential struct, 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: compiles AuthGate once per stream and routes through run_http_proxy whenever forwarded-headers OR auth-gate is set.
  • core/src/net/net_controller.rs, core/src/db/model/public.rs: default auth: None on AddSslOptions literals.
  • sdk/base/lib/osBindings/: regenerated bindings (new ProxyAuth.ts, new BasicCredential.ts, updated AddSslOptions.ts).
  • sdk/base/lib/interfaces/Host.ts: re-exports ProxyAuth and BasicCredential. bindPortForKnown defaults auth: null so package devs override per-binding via the addSsl partial.
  • container-runtime/, web/projects/ui/src/app/services/api/mock-patch.ts: default auth: null on existing literals.

Verification

  • cargo check -p start-os --lib
  • cargo test --lib --features test net:: (incl. all export_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

  • HTTP/1.1 → HTTP/2 upgrade path: the inner run_http2_proxy is called with add_forwarded=false, gate=None — the outer hop already validated the request, and the inner call only carries the upgraded byte stream.
  • No SDK version bump in this PR.

@helix-nine helix-nine changed the title feat(net): inject Bearer/Basic Authorization on upstream proxy requests feat(net): reverse-proxy auth gate (Bearer / Basic) with X-Forwarded-User Apr 29, 2026
Comment thread core/src/net/host/binding.rs Outdated
Comment on lines +245 to +246
/// Setting this implies HTTP-aware proxying (same path as
/// `add_x_forwarded_headers`).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make a similar doc comment on add_x_forwarded_headers, and drop the "same path as" parenthetical

@helix-nine
Copy link
Copy Markdown
Author

Done — pushed as 2016d65ec.

  • Added a parallel doc comment to add_x_forwarded_headers describing what it actually does (X-Forwarded-Proto: https + X-Forwarded-For: <client-ip>, and that it implies HTTP-aware proxying). Also dropped the stale // TODO.
  • Dropped the (same path as add_x_forwarded_headers) parenthetical from auth's doc — both fields are now self-documenting so the cross-reference isn't needed.
  • Regenerated sdk/base/lib/osBindings/AddSslOptions.ts so the same docs surface in the TS bindings for package developers.

@helix-nine
Copy link
Copy Markdown
Author

Thanks for the review, @dr-bonez! 🙏

Helix 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.
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