Files
traefik-llmgateway/secrets.go
T
lukaszraczylo 4b849fd6c7 fix: force include_usage, auth event logging, credential-strip coverage
C1 (quota bypass): a client sending stream_options.include_usage=false
suppressed upstream usage reporting and escaped every token/cost budget,
since the non-streaming T12 estimate never covers a streaming response.
chatCompletion (provider_openai.go) now always forces include_usage=true
on the outgoing request, merged into a copy of the client's own
stream_options map so the caller's map is never mutated in place.
clientAskedUsage is derived from the client's own value via isTruthy, so
the usage-only chunk is still suppressed for a client that never asked
for it.

I1 (vacuous security tests): the X-Api-Key credential-strip set had zero
effective coverage — existing tests either never sent x-api-key, or
targeted anthropic, whose injectAuth overwrites x-api-key regardless of
whether the strip ran. Added one openai-passthrough and one MCP-proxy
test that authenticate via x-api-key itself and assert it arrives empty
upstream. Mutation-checked: both fail when "X-Api-Key": true is removed
from gatewayCredentialHeaders (verified, then restored).

I3 (auth-event logging): added Gateway.logAuthEvent (logger.go), called
at all six authStore.identify call sites (llmgateway.go's ServeHTTP,
routes_unified.go's handleModels). A failed attempt logs method, path,
and remote address; a success logs the resolved user's name and route.
Neither line ever includes the presented API key.

M2 (usage.estimated): runUnified now reads result.estimated and logs the
estimate explicitly when a non-streaming response's usage falls back to
the body-size heuristic, making the field genuinely read — removes the
stale //nolint:unused on limits.go's usage struct.

M8 (gosec consistency): secrets.go's file: secret path read now carries
the same "#nosec G304 -- operator-supplied file path from middleware
config" annotation as users_file.go's equivalent read.

Doc nit: mcp_a2a.go's validateTargetURLs comment now also names the
dot-only ("."/"..") rule validateConfigName enforces, not just character
set and reserved names.

Gates: go test ./... -count=1 (-race) green, gofmt -l . empty, go vet
clean, make yaegi-check OK, golangci-lint (gosec+staticcheck+unused+
errcheck+govet, global config) 0 issues.
2026-08-20 14:59:21 +01:00

43 lines
1.3 KiB
Go

package traefikllmgateway
import (
"fmt"
"os"
"strings"
)
// resolveSecret resolves a config string value to its underlying secret. An
// empty value returns empty with no error. A "env:NAME" value returns
// os.Getenv(NAME), erroring if the variable is unset or empty. A
// "file:/path" value returns the file's contents with surrounding
// whitespace trimmed, erroring if the file cannot be read or its trimmed
// contents are empty — symmetric with the env: case. Any other value is
// returned unchanged as a literal.
//
// Error messages never include a resolved secret value — only the env var
// name or file path, which are config, not secrets.
func resolveSecret(v string) (string, error) {
if v == "" {
return "", nil
}
if name, ok := strings.CutPrefix(v, "env:"); ok {
val := os.Getenv(name)
if val == "" {
return "", fmt.Errorf("llmgateway: env var %q is unset or empty", name)
}
return val, nil
}
if path, ok := strings.CutPrefix(v, "file:"); ok {
b, err := os.ReadFile(path) // #nosec G304 -- operator-supplied file path from middleware config
if err != nil {
return "", fmt.Errorf("llmgateway: cannot read secret file %q: %w", path, err)
}
val := strings.TrimSpace(string(b))
if val == "" {
return "", fmt.Errorf("llmgateway: secret file %q is empty or whitespace-only", path)
}
return val, nil
}
return v, nil
}