fleet/server/platform/endpointer/clientip.go
Scott Gress 393531b624
Implement trusted proxies config (#38471)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #

# Details

Adds a new `FLEET_SERVER_TRUSTED_PROXIES` config, allowing more
fine-grained control over how the client IP is determined for requests.
Uses the
[realclientip-go](https://github.com/realclientip/realclientip-go)
library as the engine for parsing headers and using rules to determine
the IP.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced FLEET_SERVER_TRUSTED_PROXIES configuration option to
specify trusted proxy IPs and hosts. The server now supports flexible
client IP detection strategies that respect your proxy configuration,
with support for multiple formats including single IP header names, hop
counts, and IP address ranges, adapting to various infrastructure setups
and deployment scenarios.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-19 22:13:37 -06:00

85 lines
3.2 KiB
Go

package endpointer
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/realclientip/realclientip-go"
)
// NewClientIPStrategy creates a ClientIPStrategy based on the trusted_proxies configuration.
//
// Config values:
// - "" (empty): Legacy behavior for backwards compatibility - trusts True-Client-IP,
// X-Real-IP, and leftmost X-Forwarded-For. This is deprecated; use "none" when
// exposing the server directly to the internet.
// - "none": Ignores all headers, uses only RemoteAddr.
// - A header name prefixed with `header:` (e.g., "header:True-Client-IP"):
// Trust this single-IP header, fall back to RemoteAddr.
// - A number (e.g., "2"): Trust X-Forwarded-For with this many proxy hops
// - Comma-separated IPs/CIDRs (e.g., "10.0.0.0/8,192.168.0.0/16"):
// Trust X-Forwarded-For from requests originating from these proxy ranges.
func NewClientIPStrategy(trustedProxies string) (realclientip.Strategy, error) {
trustedProxies = strings.TrimSpace(trustedProxies)
var strategy realclientip.Strategy
var err error
if trustedProxies == "" {
// Empty: legacy behavior for backwards compatibility.
return &legacyStrategy{}, nil
} else if strings.EqualFold(trustedProxies, "none") {
// "none": Trust no one; return (non-spoofable) RemoteAddr only.
return realclientip.RemoteAddrStrategy{}, nil
} else if headerName, ok := strings.CutPrefix(trustedProxies, "header:"); ok {
// Check if the value is a single IP header name.
strategy, err = realclientip.NewSingleIPHeaderStrategy(headerName)
if err != nil {
return nil, fmt.Errorf("invalid header name %q: %w", trustedProxies, err)
}
} else if hopCount, err := strconv.Atoi(trustedProxies); err == nil {
// Check if it's a number (hop count).
if hopCount < 1 {
return nil, fmt.Errorf("trusted_proxies hop count must be >= 1, got %d", hopCount)
}
strategy, err = realclientip.NewRightmostTrustedCountStrategy("X-Forwarded-For", hopCount)
if err != nil {
return nil, fmt.Errorf("failed to create hop count strategy: %w", err)
}
} else {
// Otherwise, parse as comma-separated IP ranges.
rangeStrs := strings.Split(trustedProxies, ",")
for i := range rangeStrs {
rangeStrs[i] = strings.TrimSpace(rangeStrs[i])
}
trustedRanges, err := realclientip.AddressesAndRangesToIPNets(rangeStrs...)
if err != nil {
return nil, fmt.Errorf("invalid trusted_proxies IP ranges: %w", err)
}
strategy, err = realclientip.NewRightmostTrustedRangeStrategy("X-Forwarded-For", trustedRanges)
if err != nil {
return nil, fmt.Errorf("failed to create IP range strategy: %w", err)
}
}
// Chain strategy with RemoteAddr as fallback.
return realclientip.NewChainStrategy(strategy, realclientip.RemoteAddrStrategy{}), nil
}
// legacyStrategy implements the original ExtractIP behavior for backwards compatibility.
// This is deprecated; if your server is exposed directly to the internet, switch to
// the "none" strategy.
type legacyStrategy struct{}
func (s *legacyStrategy) ClientIP(headers http.Header, remoteAddr string) string {
// Build a minimal http.Request to pass to extractIP
r := &http.Request{
Header: headers,
RemoteAddr: remoteAddr,
}
return extractIP(r)
}