diff --git a/internal/config/config.go b/internal/config/config.go index 324f026..e5b2e57 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -122,8 +122,11 @@ type Config struct { // forwarding headers may be believed. Forwarded headers are honored // only when the immediate peer falls inside one of these ranges; // otherwise the peer address is used and the headers are ignored, so - // an untrusted client cannot spoof its address. Empty means trust - // nothing and always use the peer address. + // an untrusted client cannot spoof its address. An omitted key + // defaults to the RFC 1918 private ranges (see defaultTrustedProxies), + // since pixa is deployed behind a proxy on a private network; an + // explicitly empty list trusts nothing and always uses the peer + // address, and an explicit list replaces the default. TrustedProxies []netip.Prefix // CacheMaxBytes is the disk cache size limit in bytes. Zero @@ -204,6 +207,14 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) { return nil, err } + // parseCIDRList returns a nil slice only when the key is absent; an + // explicitly empty list ([]) comes back non-nil and empty. An omitted + // key takes the RFC 1918 default, while an explicit empty list is left + // as trust-nothing. + if trustedProxies == nil { + trustedProxies = defaultTrustedProxies() + } + loader := &strictLoader{sc: sc} c := &Config{ @@ -839,6 +850,21 @@ func getStringSlice(sc *smartconfig.Config) []string { // wrong type, an empty entry, a non-string entry, or an unparseable CIDR // aborts startup naming the key and the offending value; the default // (an empty list) applies only to an omitted key. +// defaultTrustedProxies returns the trusted_proxies default: the three RFC +// 1918 private ranges. pixa is always deployed behind a TLS-terminating +// reverse proxy, which in practice sits on a private network, so its +// forwarding headers are believed unless the operator says otherwise. +// Loopback is deliberately excluded: it is not an RFC 1918 range, and no +// deployment reaches pixa over it. A fresh slice is returned on each call so +// callers may hold it without aliasing shared state. +func defaultTrustedProxies() []netip.Prefix { + return []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("172.16.0.0/12"), + netip.MustParsePrefix("192.168.0.0/16"), + } +} + func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) { if sc == nil { return nil, nil