diff --git a/infrastructure_files/getting-started-enterprise.sh b/infrastructure_files/getting-started-enterprise.sh index 701598a60..e88436e84 100755 --- a/infrastructure_files/getting-started-enterprise.sh +++ b/infrastructure_files/getting-started-enterprise.sh @@ -808,9 +808,8 @@ server: # Trust X-Forwarded-* only from the Traefik container's static address. Both # keys must stay in step with the ipv4_address pinned in docker-compose.yml: - # trustedPeers decides whether forwarded headers are read at all. Leaving it - # unset trusts nothing and records Traefik's own address as every peer's - # connection IP. + # trustedPeers restricts which sources may supply forwarded headers. Leaving + # it unset trusts all IPv4 and IPv6 sources. reverseProxy: trustedPeers: - "${TRAEFIK_IP}/32" diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index afbc5c282..d5c6d9dc9 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -586,9 +586,9 @@ configure_reverse_proxy() { TRUSTED_PEERS="${NETBIRD_TRUSTED_PEERS:-}" if [[ -z "$TRUSTED_PEERS" ]]; then echo "" > /dev/stderr - echo "Note: reverseProxy.trustedPeers is unset, so NetBird will use the address your" > /dev/stderr - echo "proxy connects from as each peer's connection IP. To record real client IPs," > /dev/stderr - echo "set NETBIRD_TRUSTED_PEERS to your proxy's address (e.g. 172.20.0.5/32) and re-run." > /dev/stderr + echo "Warning: reverseProxy.trustedPeers is unset, so all IPv4 and IPv6 sources" > /dev/stderr + echo "are trusted to provide forwarded client-IP headers. Set NETBIRD_TRUSTED_PEERS" > /dev/stderr + echo "to your proxy's address (e.g. 172.20.0.5/32) and re-run." > /dev/stderr echo "" > /dev/stderr fi fi diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index cd3fb9a62..ea999d82b 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -366,20 +366,24 @@ func streamInterceptor( return handler(srv, wrapped) } -// realIPOptions builds the real-IP middleware options from the reverse proxy config. +// realIPOptions builds the real-IP middleware options. // -// TrustedPeers controls which transport peers are allowed to supply forwarded-IP -// headers. If empty, forwarded headers are ignored and the transport peer address -// is used directly. Operators terminating connections at a reverse proxy should -// configure TrustedPeers with that proxy's address or network. +// Empty TrustedPeers trusts all IPv4 and IPv6 sources. Configure TrustedPeers +// with the reverse proxy address or network. // -// X-Forwarded-For is consulted first. X-Real-IP is read when X-Forwarded-For is -// absent or has no entries left after TrustedHTTPProxiesCount is applied. +// X-Forwarded-For takes precedence over X-Real-IP. func realIPOptions(cfg nbconfig.ReverseProxy) []realip.Option { - if idx := slices.IndexFunc(cfg.TrustedPeers, func(p netip.Prefix) bool { return p.Bits() == 0 }); idx >= 0 { + trustedPeers := cfg.TrustedPeers + if len(trustedPeers) == 0 { + trustedPeers = []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("::/0"), + } + } + if idx := slices.IndexFunc(trustedPeers, func(p netip.Prefix) bool { return p.Bits() == 0 }); idx >= 0 { log.WithContext(context.Background()).Warnf("TrustedPeers contains the default route %s, which trusts "+ "X-Forwarded-For from every client and allows connection IP spoofing. Set TrustedPeers to the address "+ - "of your reverse proxy, or leave it empty to use the connection's source address.", cfg.TrustedPeers[idx]) + "of your reverse proxy.", trustedPeers[idx]) } if cfg.TrustedHTTPProxiesCount > 0 { log.WithContext(context.Background()).Warn( @@ -389,7 +393,7 @@ func realIPOptions(cfg nbconfig.ReverseProxy) []realip.Option { } return []realip.Option{ - realip.WithTrustedPeers(cfg.TrustedPeers), + realip.WithTrustedPeers(trustedPeers), realip.WithTrustedProxies(cfg.TrustedHTTPProxies), realip.WithTrustedProxiesCount(cfg.TrustedHTTPProxiesCount), realip.WithHeaders([]string{realip.XForwardedFor, realip.XRealIp}), diff --git a/management/internals/server/realip_test.go b/management/internals/server/realip_test.go index 661cbc94a..ef6b3123d 100644 --- a/management/internals/server/realip_test.go +++ b/management/internals/server/realip_test.go @@ -9,13 +9,14 @@ import ( "time" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip" - nbconfig "github.com/netbirdio/netbird/management/internals/server/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" + + nbconfig "github.com/netbirdio/netbird/management/internals/server/config" ) const ( @@ -135,8 +136,8 @@ func assertRealIP(t *testing.T, cfg nbconfig.ReverseProxy, want string, kv ...st }) } -func TestRealIPDefaultIgnoresClientForwardedHeaders(t *testing.T) { - assertRealIP(t, nbconfig.ReverseProxy{}, "127.0.0.1", +func TestRealIPDefaultTrustsForwardedHeaders(t *testing.T) { + assertRealIP(t, nbconfig.ReverseProxy{}, "203.0.113.44", realip.XForwardedFor, "203.0.113.44", realip.XRealIp, "203.0.113.44", )