Files
traefik-llmgateway/logger.go
T
lukaszraczylo 87ebee9003 fix(auth): throttle repeated auth failures, rate-limit failure log
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).
2026-08-22 15:26:25 +01:00

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
}