mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
Fixes finding 3 from the 2026-08-22 security audit: authStore.identify had no counter, no backoff, and no lockout on failed attempts, and logger.go wrote one unbuffered stderr line per attempt including the caller's RemoteAddr. An unauthenticated attacker got unlimited digest guesses at line rate and could drive synchronous stderr writes on the shared Traefik ingress. - Add per-source-IP failure throttling to identify, reusing the existing in-process memoryStore (limits.go) as authStore's own dedicated instance rather than a new dependency: authFailureLimit (20) failed attempts per authFailureWindow (1 minute) throttles further attempts from that IP outright, including a subsequently valid key, and resets once the window elapses. - clientIP reads r.RemoteAddr only, never a client-supplied header, matching the same trust boundary the rest of this package already assumes (routes_passthrough.go strips X-Forwarded-*/X-Auth-* before any outbound request). - Rate-limit logAuthEvent's failure line via authStore.shouldLogAuthFailure, the identical technique limiter.logStoreError already applies to store errors. Successful-auth logging stays unbounded (bounded by legitimate, already-authenticated traffic elsewhere).
57 lines
2.1 KiB
Go
57 lines
2.1 KiB
Go
package traefikllmgateway
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
)
|
|
|
|
// logf writes an info-level log line to stderr. No timestamps — Traefik
|
|
// adds its own. Never log key material.
|
|
func (g *Gateway) logf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "llmgw[%s] INFO %s\n", g.name, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// errorf writes an error-level log line to stderr. No timestamps — Traefik
|
|
// adds its own. Never log key material.
|
|
func (g *Gateway) errorf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "llmgw[%s] ERROR %s\n", g.name, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// logAuthEvent logs one authentication attempt, at the point one of
|
|
// authStore.identify's six call sites resolves it. A failure logs the
|
|
// method, path, and remote address the request came from; a success logs
|
|
// the resolved user's name alongside the same method and path. Neither
|
|
// line ever includes the presented API key itself — identify resolves a
|
|
// key to a *user and hands this function only that user's name, never the
|
|
// key it authenticated.
|
|
//
|
|
// The failure line is rate-limited via g.auth.shouldLogAuthFailure
|
|
// (auth.go), the same technique limiter.logStoreError (limits.go) already
|
|
// applies to store-error lines (security audit finding 3, 2026-08-22): an
|
|
// unauthenticated caller driving unlimited failed attempts must never be
|
|
// able to drive unlimited synchronous stderr writes on the shared Traefik
|
|
// ingress — a slow log pipe would otherwise stall request handling for
|
|
// every tenant, not just the one failing auth. The success line stays
|
|
// unbounded — see shouldLogAuthFailure's own doc comment for why that is
|
|
// safe.
|
|
func (g *Gateway) logAuthEvent(ok bool, userName string, r *http.Request) {
|
|
if !ok {
|
|
if g.auth.shouldLogAuthFailure() {
|
|
g.logf("auth failed: %s %q from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
|
}
|
|
return
|
|
}
|
|
g.logf("auth ok: user %q %s %q", userName, r.Method, r.URL.Path)
|
|
}
|
|
|
|
// authEventUserName extracts u's name for logAuthEvent's userName
|
|
// argument, or "" when u is nil — identify's failure path, which never
|
|
// resolves a user.
|
|
func authEventUserName(u *user) string {
|
|
if u == nil {
|
|
return ""
|
|
}
|
|
return u.name
|
|
}
|