Skip to content

feat: BYOK auth-passthrough — forward per-caller credentials to upstreams#12

Open
SPIKESPIGEL404 wants to merge 1 commit into
Dstack-TEE:mainfrom
SPIKESPIGEL404:feat/byok-auth-passthrough
Open

feat: BYOK auth-passthrough — forward per-caller credentials to upstreams#12
SPIKESPIGEL404 wants to merge 1 commit into
Dstack-TEE:mainfrom
SPIKESPIGEL404:feat/byok-auth-passthrough

Conversation

@SPIKESPIGEL404

Copy link
Copy Markdown

Closes #11.

Summary

Adds an opt-in auth_passthrough mode to upstream config. When enabled, the gateway forwards each caller's own Authorization credential to the upstream instead of a statically-configured bearer_token.

This enables BYOK (bring-your-own-key): a fronting service can let each end user supply their own upstream provider API key, which the gateway relays per-request — without the operator holding a shared key for that upstream. Attestation, verification, channel binding, and receipts are unchanged.

Motivation

Today an upstream authenticates with a single operator-configured bearer_token (OpenAICompatibleBackend), and the caller's inbound Authorization is consumed only as a hashed receipt-owner tag — it never reaches the upstream. That's correct for a gateway fronting one account, but it blocks multi-tenant BYOK, where the gateway should relay each user's own provider key (e.g. a per-user NEAR AI key) while still doing the attested-confidential-inference work it exists for.

What this does

  • New UpstreamConfig.auth_passthrough: bool (default false, serde-defaulted so existing configs are unchanged).
  • When set, OpenAICompatibleBackend forwards the caller credential as the upstream Authorization and never uses a static token.
  • The caller credential is threaded handler → service → backend via a new UpstreamRequest.client_authorization.

Security / design notes

This change moves a raw secret (the caller's bearer) along new code paths, so the handling is deliberately conservative:

  • Redaction is structural, end-to-end. The credential is wrapped in a ClientAuthorization newtype whose hand-written Debug redacts, and it is carried as that type across every hop (ChatCompletionRequest, BackendForwardInput, UpstreamRequest). No struct carrying it derives Serialize, and none is logged. The raw key cannot reach logs, receipts, or the request store.
  • Fail-closed precedence. In passthrough mode the static bearer_token is never sent. Config validation rejects setting both auth_passthrough and bearer_token on the same upstream, and an absent caller credential yields no Authorization header (a visible upstream rejection), never a silent fallback.
  • Provider support is validated. auth_passthrough is only honored by providers served through OpenAICompatibleBackend (openai-compatible, aci-dcap, tinfoil, near-ai, phala-direct). Enabling it on a provider that ignores it (chutes, which uses its own E2EE transport) is rejected at config validation rather than silently no-op'ing.
  • Middleware mode is unsupported, and fails closed. The middleware request store keeps only a hashed requester, so the raw credential isn't available on that path. The gateway now refuses to start if middleware mode is enabled together with any auth_passthrough upstream, rather than 401-ing every request at runtime.
  • Credential normalization. extract_bearer strips/trims the Bearer scheme and the backend re-wraps it, so only a well-formed Bearer <token> is forwarded (no header-shape passthrough).

Scope / limitations

  • Applies to the forwarding (POST) path. Server-initiated upstream GETs (e.g. model listing) are unaffected — under config-driven routing the model list is synthesized locally, so no per-caller credential exists there.
  • In BYOK mode the receipt-owner tag becomes sha256(caller credential) (same as today's hashing of the inbound bearer); ownership semantics therefore bind to the provider key in this mode.
  • Not wired for the Chutes E2EE transport (rejected at validation, see above).

Testing

  • tests/provider_e2e.rs: new end-to-end test asserting the load-bearing properties against the mock-upstream harness — the caller credential reaches the upstream verbatim, and an absent credential results in no Authorization header (no static-token fallback).
  • src/aggregator/upstream_config.rs: unit tests for the new validation rules (auth_passthrough defaults off and parses; rejected with a static bearer_token; rejected for an unsupported provider).
  • cargo check --all-targets, cargo clippy --all-targets, and cargo fmt -- --check are clean. Full suite passes (lib + provider_e2e + aci_service_surface).

Backward compatibility

auth_passthrough is serde-defaulted to false; existing upstream configs and behavior are unchanged. PublicUpstreamConfig gains the field so the admin/read API reflects it.

…eams

Adds an opt-in per-upstream `auth_passthrough` mode: the gateway forwards
each caller's own `Authorization` to the upstream instead of a static
`bearer_token`. Enables multi-tenant BYOK (e.g. per-user NEAR AI keys)
while attestation, verification, channel binding, and receipts are
unchanged. Closes Dstack-TEE#11.

- `UpstreamConfig.auth_passthrough` (default false); rejected at config
  validation if combined with a static `bearer_token`, and rejected for
  providers whose backend ignores it (Chutes).
- `OpenAICompatibleBackend` forwards only the caller credential in
  passthrough mode and never the static token (absent credential => no
  auth header, a visible upstream rejection).
- Caller credential threaded handler -> service -> backend via
  `UpstreamRequest.client_authorization`, a `ClientAuthorization` newtype
  whose Debug redacts so a raw key cannot reach logs or receipts.
- Middleware mode is unsupported (the request store keeps only a hashed
  requester) and fails closed at startup.

Tests: config parse/validation (defaults off, rejects static token,
rejects unsupported provider) + an e2e forwarding test asserting the
caller credential reaches the upstream verbatim and an absent credential
yields no Authorization header. check/clippy/fmt clean; lib + provider_e2e
+ surface suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@SPIKESPIGEL404 SPIKESPIGEL404 force-pushed the feat/byok-auth-passthrough branch from c9c7b53 to 5cf3cfc Compare June 13, 2026 01:28
@h4x3rotab

h4x3rotab commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for this, and for the clean PR. My pushback is about where it lives, not whether it's useful.

I don't want per-caller credentials handled inside the gateway core. Whose token gets forwarded is provider policy, and the gateway's job is to stay a thin, attestable base: verify upstreams, let clients verify the gateway, and host a middleware for exactly this kind of custom logic.

Your middleware already receives the caller's Authorization header today. What's missing is the mechanism to pass it on: a middleware can't yet hand the gateway a per-request upstream credential, and the backend overrides it with the operator key. I'd rather add that mechanism than merge BYOK into the core. I wrote up the details and am tracking the plumbing in #11.

Leaving this open. Let me know if that direction works for you.

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.

Middleware plumbing: per-request upstream auth so a middleware can forward caller credentials (BYOK)

2 participants