feat(passthrough): add model, path, and header authorization hardening

handlePassthrough checked only the provider allowlist, never the model
or the path. A tenant scoped to one cheap model could reach a
provider's entire native API (files, batches, fine-tuning) on the
operator's key, and client identity headers reached keyless in-cluster
MCP/A2A targets unfiltered. This change closes both gaps, additively:
every new field defaults to reproducing prior behavior exactly.

Model enforcement: when a passthrough JSON body carries a top-level
"model" field, handlePassthrough checks it against the group's model
glob, in both the bare and provider-prefixed form (the same
dual-candidate matcher the unified route's resolveAgainst already
applies). A body with no inspectable model (Gemini's URL-embedded
model, a multipart upload, a non-JSON body) falls back to
provider-only authorization, unchanged from before this round.

Per-provider toggle: ProviderConfig.Passthrough, nil or true by
default, lets an operator disable a provider's native passthrough
route. A disabled provider now reports the ordinary unknown-route 404,
checked in ServeHTTP's own route gate, before auth even runs.

Per-group path allowlist: GroupConfig.PassthroughPaths, empty by
default (matching every other allow-list field's empty-means-all
semantics), restricts which native rest path a group may address.

Dangerous-header stripping: proxyUpstream now additionally strips
Forwarded, X-Forwarded-*, X-Auth-*, X-Remote-User, X-Remote-Groups, and
Cookie on both the native-passthrough and MCP/A2A target-proxy paths,
plus OpenAI-Organization, OpenAI-Project, and anthropic-beta on native
passthrough only. This is an additive denylist expansion, not an
allowlist inversion; ordinary headers still pass through.

A dedicated test asserts a config with none of the new fields set
behaves identically to before this change. The operator's home group
(providers/models/mcpServers/agents/passthroughPaths all empty) stays
fully unrestricted, and macstudio-rerank/parakeet-mlx/piper-tts stay
reachable through native passthrough by default.
This commit is contained in:
2026-08-22 11:40:27 +01:00
parent a418814b34
commit e3ef4084d2
9 changed files with 766 additions and 57 deletions
+18
View File
@@ -386,6 +386,24 @@ func validateMetadataPath(providerName, metadataPath string) error {
return nil
}
// providerPassthroughEnabled reports whether providerName's native
// passthrough route is reachable (security+performance audit,
// 2026-08-22): true when ProviderConfig.Passthrough is nil (the default)
// or explicitly true; false only when an operator set it to false.
// cfg.Providers[providerName] is assumed to already be a known key —
// ServeHTTP's own passthrough route gate (llmgateway.go) checks
// g.adapters[providerName] first, and buildAdapters guarantees adapters
// and cfg.Providers share the same key set — but a missing key still
// defaults to true here rather than panicking, matching every other
// nil-means-default helper in this package.
func providerPassthroughEnabled(cfg *Config, providerName string) bool {
pc, ok := cfg.Providers[providerName]
if !ok || pc.Passthrough == nil {
return true
}
return *pc.Passthrough
}
// buildAdapters resolves cfg.Providers into a map of providerAdapter keyed
// by provider name. Every provider name is validated (validateConfigName)
// and every ProviderConfig value must be non-nil before anything else runs,