mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
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:
@@ -302,6 +302,29 @@ func TestGroup_AllowsModel_EmptyListAllowsAnything(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroup_AllowsPassthroughPath_EmptyListAllowsAll pins the zero-config
|
||||
// default for GroupConfig.PassthroughPaths (security+performance audit,
|
||||
// 2026-08-22): a group with no PassthroughPaths configured — every group
|
||||
// that existed before this field did — must keep reaching every native
|
||||
// passthrough path, exactly like allowsProvider/allowsModel/allowsMCP/
|
||||
// allowsAgent's own empty-means-all defaults.
|
||||
func TestGroup_AllowsPassthroughPath_EmptyListAllowsAll(t *testing.T) {
|
||||
grp := &group{name: "eng"}
|
||||
if !grp.allowsPassthroughPath("v1/files") {
|
||||
t.Fatal("empty passthroughPaths list should allow all")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroup_AllowsPassthroughPath_Glob(t *testing.T) {
|
||||
grp := &group{name: "eng", passthroughPaths: []string{"v1/chat/completions"}}
|
||||
if !grp.allowsPassthroughPath("v1/chat/completions") {
|
||||
t.Error("want exact match")
|
||||
}
|
||||
if grp.allowsPassthroughPath("v1/files") {
|
||||
t.Error("want no match for a path outside the allowlist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthStore_ReplaceFileUsers_AddsAndSwapsUsers(t *testing.T) {
|
||||
a, err := newAuthStore(testAuthCfg()) // inline user "a" / sk-secret in group eng
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user