feat: default trusted_proxies to the rfc1918 private ranges

An omitted trusted_proxies key now defaults to 10.0.0.0/8, 172.16.0.0/12
and 192.168.0.0/16 instead of trusting no one, matching the deployment
where pixa sits behind a proxy on a private network. An explicitly empty
list still trusts nothing, and an explicit list replaces the default;
these are distinguished by parseCIDRList returning nil only for an absent
key. Loopback is excluded, as it is not an RFC 1918 range.

Model: opus-4-8
This commit is contained in:
2026-09-22 06:57:00 +00:00
parent 9a5e7123ec
commit e0635a7f0b
+28 -2
View File
@@ -122,8 +122,11 @@ type Config struct {
// forwarding headers may be believed. Forwarded headers are honored // forwarding headers may be believed. Forwarded headers are honored
// only when the immediate peer falls inside one of these ranges; // only when the immediate peer falls inside one of these ranges;
// otherwise the peer address is used and the headers are ignored, so // otherwise the peer address is used and the headers are ignored, so
// an untrusted client cannot spoof its address. Empty means trust // an untrusted client cannot spoof its address. An omitted key
// nothing and always use the peer address. // 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 TrustedProxies []netip.Prefix
// CacheMaxBytes is the disk cache size limit in bytes. Zero // CacheMaxBytes is the disk cache size limit in bytes. Zero
@@ -204,6 +207,14 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
return nil, err 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} loader := &strictLoader{sc: sc}
c := &Config{ 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 // wrong type, an empty entry, a non-string entry, or an unparseable CIDR
// aborts startup naming the key and the offending value; the default // aborts startup naming the key and the offending value; the default
// (an empty list) applies only to an omitted key. // (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) { func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) {
if sc == nil { if sc == nil {
return nil, nil return nil, nil