feat: BYOK auth-passthrough — forward per-caller credentials to upstreams#12
feat: BYOK auth-passthrough — forward per-caller credentials to upstreams#12SPIKESPIGEL404 wants to merge 1 commit into
Conversation
…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>
c9c7b53 to
5cf3cfc
Compare
|
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 Leaving this open. Let me know if that direction works for you. |
Closes #11.
Summary
Adds an opt-in
auth_passthroughmode to upstream config. When enabled, the gateway forwards each caller's ownAuthorizationcredential to the upstream instead of a statically-configuredbearer_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 inboundAuthorizationis 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
UpstreamConfig.auth_passthrough: bool(defaultfalse, serde-defaulted so existing configs are unchanged).OpenAICompatibleBackendforwards the caller credential as the upstreamAuthorizationand never uses a static token.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:
ClientAuthorizationnewtype whose hand-writtenDebugredacts, and it is carried as that type across every hop (ChatCompletionRequest,BackendForwardInput,UpstreamRequest). No struct carrying it derivesSerialize, and none is logged. The raw key cannot reach logs, receipts, or the request store.bearer_tokenis never sent. Config validation rejects setting bothauth_passthroughandbearer_tokenon the same upstream, and an absent caller credential yields noAuthorizationheader (a visible upstream rejection), never a silent fallback.auth_passthroughis only honored by providers served throughOpenAICompatibleBackend(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.auth_passthroughupstream, rather than 401-ing every request at runtime.extract_bearerstrips/trims theBearerscheme and the backend re-wraps it, so only a well-formedBearer <token>is forwarded (no header-shape passthrough).Scope / limitations
sha256(caller credential)(same as today's hashing of the inbound bearer); ownership semantics therefore bind to the provider key in this mode.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 noAuthorizationheader (no static-token fallback).src/aggregator/upstream_config.rs: unit tests for the new validation rules (auth_passthroughdefaults off and parses; rejected with a staticbearer_token; rejected for an unsupported provider).cargo check --all-targets,cargo clippy --all-targets, andcargo fmt -- --checkare clean. Full suite passes (lib +provider_e2e+aci_service_surface).Backward compatibility
auth_passthroughis serde-defaulted tofalse; existing upstream configs and behavior are unchanged.PublicUpstreamConfiggains the field so the admin/read API reflects it.