mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-01 04:05:54 +00:00
* s3api: add TrustedProxies allowlist helper for aws:SourceIp extraction Introduces a policy_engine.TrustedProxies type that parses a comma-separated list of bare IPs and CIDRs (mirroring Guard.UpdateWhiteList) and extracts the client IP for aws:SourceIp condition evaluation. When the direct TCP peer is in the allowlist, X-Forwarded-For is walked right-to-left skipping trusted hops (then X-Real-Ip); otherwise the direct peer address is returned. This is the building block for restoring configurable forwarded-header trust removed inb88156f(#11231), as proposed in #11302. * s3api: honor trusted-proxy allowlist in bucket/IAM policy engine Make ExtractConditionValuesFromRequest a method on *PolicyEngine so it can use the engine TrustedProxies when resolving aws:SourceIp. With no allowlist configured the behavior is unchanged fromb88156f: the direct TCP peer is used and forwarded headers are ignored. When an allowlist is configured via SetTrustedProxies, requests from a trusted peer honor X-Forwarded-For (right-to-left) then X-Real-Ip. Update the two call sites (auth_credentials.go, s3api_bucket_policy_engine.go) and the engine tests to the method form, and add a regression test for the trusted-proxy path. * s3api: honor trusted-proxy allowlist in IAM role/session policies Make extractRequestContext and extractSourceIP methods on *S3IAMIntegration so they can use the integration TrustedProxies when resolving aws:SourceIp. With no allowlist configured the behavior is unchanged fromb88156f: the direct TCP peer is used and forwarded headers are ignored. When an allowlist is configured via SetTrustedProxies, requests from a trusted peer honor X-Forwarded-For (right-to-left) then X-Real-Ip. Update the call site in isActionExplicitlyDeniedByIAM to type-assert the integration and use the method, and add a regression test for the trusted-proxy path. * s3api: load [s3.trusted_proxies] from security.toml and wire to engines Read s3.trusted_proxies.white_list (comma-separated IPs/CIDRs) from security.toml and propagate the allowlist to the bucket policy engine, the IAM policy engine (persisted across rebuilds via IdentityAccessManagement.SetTrustedProxies), and the IAM integration. Reloaded on SIGHUP alongside the JWT signing keys. Document the new section in the scaffold security.toml. Closes #11302. * s3api: harden TrustedProxies parsing and X-Forwarded-For traversal Canonicalize bare IP entries (via net.ParseIP + String) so non-canonical IPv6 allowlist entries such as 2001:0db8::1 match peers rendered as 2001:db8::1, and log+skip unparseable bare entries instead of storing them inertly. When walking X-Forwarded-For right-to-left, stop at the first malformed (non-empty, unparseable) entry instead of skipping it, and only fall back to the leftmost valid IP when the chain was well-formed. This prevents a malformed hop from masking a forged IP to its left. Addresses review feedback on #11315. * s3api: make TrustedProxies reload race-free via atomic.Pointer Store the trusted-proxy allowlist behind sync/atomic.Pointer in PolicyEngine and S3IAMIntegration so SIGHUP reloads (which swap the allowlist) cannot race with concurrent request handlers reading it. This mirrors the existing Guard guardState pattern. The IdentityAccessManagement copy is already protected by iam.m. Addresses review feedback on #11315.
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
package policy_engine
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
func newReq(remoteAddr string, xff, xRealIP string) *http.Request {
|
|
r := &http.Request{RemoteAddr: remoteAddr, Header: http.Header{}}
|
|
if xff != "" {
|
|
r.Header.Set("X-Forwarded-For", xff)
|
|
}
|
|
if xRealIP != "" {
|
|
r.Header.Set("X-Real-Ip", xRealIP)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestTrustedProxies_IsTrusted(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.5", "192.168.0.0/16"})
|
|
if !tp.IsTrusted("10.0.0.5") {
|
|
t.Error("bare IP should be trusted")
|
|
}
|
|
if !tp.IsTrusted("192.168.1.100") {
|
|
t.Error("IP in CIDR should be trusted")
|
|
}
|
|
if tp.IsTrusted("8.8.8.8") {
|
|
t.Error("public IP should not be trusted")
|
|
}
|
|
if tp.IsTrusted("not-an-ip") {
|
|
t.Error("invalid IP should not be trusted")
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_CanonicalizesBareIPv6(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"2001:0db8::1"})
|
|
if !tp.IsTrusted("2001:db8::1") {
|
|
t.Error("canonical IPv6 should match non-canonical allowlist entry")
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_InvalidBareIPSkipped(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"not-an-ip", "10.0.0.5"})
|
|
if tp.IsTrusted("not-an-ip") {
|
|
t.Error("invalid entry should not be stored")
|
|
}
|
|
if !tp.IsTrusted("10.0.0.5") {
|
|
t.Error("valid entry after invalid one should still load")
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_NilNotTrusted(t *testing.T) {
|
|
var tp *TrustedProxies
|
|
if tp.IsTrusted("127.0.0.1") {
|
|
t.Error("nil TrustedProxies should not trust any IP")
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_DirectPeerWhenUntrusted(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("203.0.113.5:1234", "8.8.8.8", "1.1.1.1")
|
|
if got := tp.ExtractSourceIP(r); got != "203.0.113.5" {
|
|
t.Errorf("untrusted peer: want 203.0.113.5, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_XForwardedFor(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("10.0.0.1:1234", "8.8.8.8, 10.0.0.2", "")
|
|
if got := tp.ExtractSourceIP(r); got != "8.8.8.8" {
|
|
t.Errorf("trusted proxy: want 8.8.8.8, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_AllTrustedReturnsLeftmost(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("10.0.0.1:1234", "10.0.0.5, 10.0.0.6", "")
|
|
if got := tp.ExtractSourceIP(r); got != "10.0.0.5" {
|
|
t.Errorf("all-trusted chain: want leftmost 10.0.0.5, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_MalformedXFFFallsBackToPeer(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("10.0.0.1:1234", "8.8.8.8, garbage", "")
|
|
if got := tp.ExtractSourceIP(r); got != "10.0.0.1" {
|
|
t.Errorf("malformed XFF: want direct peer 10.0.0.1, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_XRealIP(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("10.0.0.1:1234", "", "8.8.8.8")
|
|
if got := tp.ExtractSourceIP(r); got != "8.8.8.8" {
|
|
t.Errorf("X-Real-Ip: want 8.8.8.8, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_XForwardedForPreferredOverXRealIP(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("10.0.0.1:1234", "8.8.8.8", "1.1.1.1")
|
|
if got := tp.ExtractSourceIP(r); got != "8.8.8.8" {
|
|
t.Errorf("XFF should win over X-Real-Ip: want 8.8.8.8, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_NoTrustedProxiesUsesPeer(t *testing.T) {
|
|
tp := NewTrustedProxies(nil)
|
|
r := newReq("10.0.0.1:1234", "8.8.8.8", "1.1.1.1")
|
|
if got := tp.ExtractSourceIP(r); got != "10.0.0.1" {
|
|
t.Errorf("empty allowlist: want 10.0.0.1, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_NilRequest(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
if got := tp.ExtractSourceIP(nil); got != "" {
|
|
t.Errorf("nil request: want empty, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxies_ExtractSourceIP_UnixSocket(t *testing.T) {
|
|
tp := NewTrustedProxies([]string{"10.0.0.0/24"})
|
|
r := newReq("@", "", "")
|
|
if got := tp.ExtractSourceIP(r); got != "@" {
|
|
t.Errorf("unix socket: want @, got %s", got)
|
|
}
|
|
}
|