munin/internal/auth/trustedproxy.go
2026-09-15 20:36:26 +02:00

179 lines
5.6 KiB
Go

// Dieses File regelt, wann dem Header X-Forwarded-For geglaubt wird.
//
// # Das Problem
//
// Läuft das Wiki direkt am Netz, ist r.RemoteAddr die echte IP des Besuchers.
// Läuft davor ein Reverse-Proxy (nginx, Caddy, Traefik, Cloudflare), sieht der
// Server nur noch die IP des Proxys — für alle Besucher dieselbe. Das
// IP-Rate-Limiting (ratelimit.go) wäre damit wirkungslos: entweder es sperrt
// niemanden aus, oder es sperrt beim ersten Angreifer gleich alle.
//
// Der Proxy schickt die echte IP deshalb im Header X-Forwarded-For mit.
//
// # Warum man diesem Header nicht einfach glauben darf
//
// Header kann jeder Client frei setzen. Würde der Server X-Forwarded-For
// bedingungslos übernehmen, bräuchte ein Angreifer nur bei jedem Login-Versuch
// eine andere Fantasie-IP zu schicken — das Rate-Limiting wäre komplett
// ausgehebelt, und das Audit-Log stünde voller erfundener Adressen.
//
// # Die Lösung
//
// Der Header wird nur ausgewertet, wenn der Request tatsächlich von einem als
// vertrauenswürdig konfigurierten Proxy kommt. Konfiguriert wird das über die
// Umgebungsvariable TRUSTED_PROXY, z.B.:
//
// TRUSTED_PROXY=127.0.0.1,10.0.0.0/8
//
// Ist sie nicht gesetzt (Standard), wird der Header ignoriert — sicher per Default.
package auth
import (
"fmt"
"net"
"net/http"
"net/netip"
"strings"
"sync"
)
// trustedProxies hält die konfigurierten Netze. Wird einmal beim Start über
// SetTrustedProxies gefüllt und danach nur noch gelesen.
//
// Ein Paket-globaler Wert ist hier vertretbar, weil clientIP() von sehr vielen
// Stellen aufgerufen wird und die Konfiguration prozessweit gilt. Der RWMutex
// schützt trotzdem sauber, falls die Konfiguration doch einmal später gesetzt wird.
var (
trustedProxiesMu sync.RWMutex
trustedProxies []netip.Prefix
)
// SetTrustedProxies liest die TRUSTED_PROXY-Konfiguration ein.
//
// Akzeptiert eine kommaseparierte Liste aus einzelnen IPs ("127.0.0.1") und
// CIDR-Netzen ("10.0.0.0/8"). Eine einzelne IP wird intern zu einem Netz mit
// voller Präfixlänge — dann gilt für beide Fälle derselbe Vergleich.
func SetTrustedProxies(value string) error {
var prefixes []netip.Prefix
for _, raw := range strings.Split(value, ",") {
entry := strings.TrimSpace(raw)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
prefix, err := netip.ParsePrefix(entry)
if err != nil {
return fmt.Errorf("TRUSTED_PROXY: %q ist kein gültiges Netz: %w", entry, err)
}
prefixes = append(prefixes, prefix)
continue
}
addr, err := netip.ParseAddr(entry)
if err != nil {
return fmt.Errorf("TRUSTED_PROXY: %q ist keine gültige IP-Adresse: %w", entry, err)
}
prefixes = append(prefixes, netip.PrefixFrom(addr, addr.BitLen()))
}
trustedProxiesMu.Lock()
defer trustedProxiesMu.Unlock()
trustedProxies = prefixes
return nil
}
// TrustedProxyCount gibt zurück wie viele Netze konfiguriert sind — nur damit
// main.go beim Start eine sinnvolle Zeile ins Log schreiben kann.
func TrustedProxyCount() int {
trustedProxiesMu.RLock()
defer trustedProxiesMu.RUnlock()
return len(trustedProxies)
}
// ClientIP ermittelt die Adresse des Aufrufers.
//
// Normalerweise ist das r.RemoteAddr. Steht ein Reverse-Proxy davor, ist das
// dessen Adresse — dann wird X-Forwarded-For ausgewertet, aber nur wenn der
// Absender über TRUSTED_PROXY als vertrauenswürdig hinterlegt ist.
func ClientIP(r *http.Request) string {
if ip, ok := forwardedClientIP(r); ok {
return ip
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
// forwardedClientIP liefert die echte Client-IP aus X-Forwarded-For — aber nur
// wenn der direkte Absender ein vertrauenswürdiger Proxy ist.
func forwardedClientIP(r *http.Request) (string, bool) {
trustedProxiesMu.RLock()
prefixes := trustedProxies
trustedProxiesMu.RUnlock()
if len(prefixes) == 0 {
return "", false
}
// Wer klopft direkt an? Nur dessen Vertrauenswürdigkeit zählt.
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return "", false
}
peer, err := netip.ParseAddr(host)
if err != nil {
return "", false
}
if !isTrustedProxy(peer, prefixes) {
return "", false
}
header := r.Header.Get("X-Forwarded-For")
if header == "" {
return "", false
}
// Format: "client, proxy1, proxy2" — ganz links steht der ursprüngliche Client.
//
// Wir gehen von rechts nach links und überspringen alle vertrauenswürdigen
// Proxys; der erste Eintrag der kein bekannter Proxy ist, ist der Client.
// Das ist wichtiger als es klingt: würde man stur den linkesten Eintrag nehmen,
// könnte ein Client einfach ein eigenes "X-Forwarded-For: 1.2.3.4" mitschicken,
// das der Proxy dann nur ergänzt — und die erfundene IP stünde ganz links.
parts := strings.Split(header, ",")
for i := len(parts) - 1; i >= 0; i-- {
candidate := strings.TrimSpace(parts[i])
addr, err := netip.ParseAddr(candidate)
if err != nil {
// Unlesbarer Eintrag — ab hier ist der Kette nicht mehr zu trauen.
return "", false
}
if isTrustedProxy(addr, prefixes) {
continue
}
return addr.String(), true
}
// Alle Einträge waren bekannte Proxys — dann bleibt nur der direkte Absender.
return "", false
}
// isTrustedProxy prüft ob eine Adresse in einem der konfigurierten Netze liegt.
func isTrustedProxy(addr netip.Addr, prefixes []netip.Prefix) bool {
// IPv4-Adressen können in IPv6-Schreibweise ankommen (::ffff:127.0.0.1).
// Unmap macht daraus wieder eine normale IPv4-Adresse, sonst schlägt der
// Vergleich mit einem IPv4-Netz fehl.
addr = addr.Unmap()
for _, prefix := range prefixes {
if prefix.Contains(addr) {
return true
}
}
return false
}