@@ -1008,7 +1008,34 @@ func withGlobalSTSLDAPLoginRateLimiterForTest(limiter *stsLDAPLoginRateLimiter,
10081008 fn ()
10091009}
10101010
1011- func (s * TestSuiteIAM ) postLDAPSTS (c * check , username , password string ) ldapSTSHTTPResult {
1011+ func withLDAPSTSTrustedProxiesForTest (t * testing.T , trustedProxies string , fn func ()) {
1012+ t .Helper ()
1013+
1014+ previousIAMSys := globalIAMSys
1015+ if globalIAMSys == nil {
1016+ globalIAMSys = & IAMSys {}
1017+ }
1018+ previous := globalIAMSys .LDAPConfig .Clone ()
1019+ if err := globalIAMSys .LDAPConfig .SetSTSTrustedProxies (trustedProxies ); err != nil {
1020+ t .Fatalf ("unable to set LDAP STS trusted proxies for test: %v" , err )
1021+ }
1022+ defer func () {
1023+ globalIAMSys .LDAPConfig = previous
1024+ globalIAMSys = previousIAMSys
1025+ }()
1026+
1027+ fn ()
1028+ }
1029+
1030+ func singleHeader (key , value string ) http.Header {
1031+ header := make (http.Header )
1032+ if key != "" {
1033+ header .Set (key , value )
1034+ }
1035+ return header
1036+ }
1037+
1038+ func (s * TestSuiteIAM ) postLDAPSTSWithHeaders (c * check , username , password string , headers http.Header ) ldapSTSHTTPResult {
10121039 c .Helper ()
10131040
10141041 ctx , cancel := context .WithTimeout (context .Background (), testDefaultTimeout )
@@ -1025,6 +1052,11 @@ func (s *TestSuiteIAM) postLDAPSTS(c *check, username, password string) ldapSTSH
10251052 c .Fatalf ("unexpected request creation error: %v" , err )
10261053 }
10271054 req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
1055+ for key , values := range headers {
1056+ for _ , value := range values {
1057+ req .Header .Add (key , value )
1058+ }
1059+ }
10281060
10291061 resp , err := s .TestSuiteCommon .client .Do (req )
10301062 if err != nil {
@@ -1044,6 +1076,11 @@ func (s *TestSuiteIAM) postLDAPSTS(c *check, username, password string) ldapSTSH
10441076 }
10451077}
10461078
1079+ func (s * TestSuiteIAM ) postLDAPSTS (c * check , username , password string ) ldapSTSHTTPResult {
1080+ c .Helper ()
1081+ return s .postLDAPSTSWithHeaders (c , username , password , nil )
1082+ }
1083+
10471084func (s * TestSuiteIAM ) postLDAPSTSForError (c * check , username , password string ) ldapSTSErrorResult {
10481085 c .Helper ()
10491086
@@ -1198,6 +1235,32 @@ func (s *TestSuiteIAM) TestLDAPSTSUpstreamFailure(c *check) {
11981235 )
11991236}
12001237
1238+ func (s * TestSuiteIAM ) TestLDAPSTSTrustedProxyRateLimit (c * check ) {
1239+ withLDAPSTSTrustedProxiesForTest (c .T , "127.0.0.0/8,::1/128" , func () {
1240+ withGlobalSTSLDAPLoginRateLimiterForTest (
1241+ newSTSLDAPLoginRateLimiter (time .Hour , 1 , stsLDAPLoginEntryTTL ),
1242+ func () {
1243+ // These usernames intentionally do not exist. The test asserts that
1244+ // LDAP user-not-found stays classified as an auth error, so failed
1245+ // attempts still commit the reservation and hit the source bucket.
1246+ first := s .postLDAPSTSWithHeaders (c , "missing-user-a" , "nottherightpassword" , singleHeader ("X-Real-IP" , "203.0.113.10" ))
1247+ second := s .postLDAPSTSWithHeaders (c , "missing-user-b" , "nottherightpassword" , singleHeader ("X-Real-IP" , "198.51.100.23" ))
1248+ third := s .postLDAPSTSWithHeaders (c , "missing-user-c" , "nottherightpassword" , singleHeader ("X-Real-IP" , "203.0.113.10" ))
1249+
1250+ if first .StatusCode != http .StatusBadRequest {
1251+ c .Fatalf ("expected first trusted-proxy LDAP STS auth failure to return %d, got %d body: %s" , http .StatusBadRequest , first .StatusCode , first .Body )
1252+ }
1253+ if second .StatusCode != http .StatusBadRequest {
1254+ c .Fatalf ("expected a different forwarded client IP behind the same trusted proxy to avoid source throttling, got %d body: %s" , second .StatusCode , second .Body )
1255+ }
1256+ if third .StatusCode != http .StatusTooManyRequests {
1257+ c .Fatalf ("expected the same forwarded client IP behind the trusted proxy to be throttled with %d, got %d body: %s" , http .StatusTooManyRequests , third .StatusCode , third .Body )
1258+ }
1259+ },
1260+ )
1261+ })
1262+ }
1263+
12011264func TestIAMWithLDAPSecurityServerSuite (t * testing.T ) {
12021265 tests := []struct {
12031266 name string
@@ -1227,6 +1290,12 @@ func TestIAMWithLDAPSecurityServerSuite(t *testing.T) {
12271290 suite .TestLDAPSTSUpstreamFailure (c )
12281291 },
12291292 },
1293+ {
1294+ name : "TrustedProxyRateLimit" ,
1295+ run : func (suite * TestSuiteIAM , c * check , ldapServer string ) {
1296+ suite .TestLDAPSTSTrustedProxyRateLimit (c )
1297+ },
1298+ },
12301299 }
12311300
12321301 for i , testCase := range iamTestSuites {
@@ -1397,6 +1466,35 @@ func TestSTSLDAPLoginRateLimiterReserveCancel(t *testing.T) {
13971466 reservation .Cancel ()
13981467}
13991468
1469+ func TestSTSLDAPLoginKeyLimiterCancelDoesNotOverCreditAfterRefill (t * testing.T ) {
1470+ set := newSTSLDAPLoginKeyLimiterSet (10 * time .Millisecond , 2 , time .Minute )
1471+ start := time .Unix (0 , 0 )
1472+
1473+ first := set .Reserve (start , "192.0.2.10" )
1474+ if first == nil {
1475+ t .Fatal ("expected first reservation to succeed" )
1476+ }
1477+ second := set .Reserve (start .Add (5 * time .Millisecond ), "192.0.2.10" )
1478+ if second == nil {
1479+ t .Fatal ("expected second reservation to succeed while one token remains available" )
1480+ }
1481+
1482+ first .CancelAt (start .Add (10 * time .Millisecond ))
1483+
1484+ third := set .Reserve (start .Add (10 * time .Millisecond ), "192.0.2.10" )
1485+ if third == nil {
1486+ t .Fatal ("expected canceled reservation to restore exactly one slot" )
1487+ }
1488+ defer third .CancelAt (start .Add (10 * time .Millisecond ))
1489+
1490+ if extra := set .Reserve (start .Add (10 * time .Millisecond ), "192.0.2.10" ); extra != nil {
1491+ extra .CancelAt (start .Add (10 * time .Millisecond ))
1492+ t .Fatal ("expected only one slot to be restored after cancel; got over-credit from refill" )
1493+ }
1494+
1495+ second .CancelAt (start .Add (10 * time .Millisecond ))
1496+ }
1497+
14001498func TestSTSLDAPLoginRateLimiterReserveRollbackOnCompositeFailure (t * testing.T ) {
14011499 limiter := newSTSLDAPLoginRateLimiter (time .Hour , 1 , time .Minute )
14021500
@@ -1531,7 +1629,7 @@ func TestGetSTSLDAPLoginSourceIPIgnoresSpoofedForwardingHeaders(t *testing.T) {
15311629 for _ , tt := range tests {
15321630 t .Run (tt .name , func (t * testing.T ) {
15331631 req := & http.Request {
1534- Header : http. Header { tt .headerKey : [] string { tt .headerValue }} ,
1632+ Header : singleHeader ( tt .headerKey , tt .headerValue ) ,
15351633 RemoteAddr : "192.0.2.10:9000" ,
15361634 }
15371635
@@ -1542,6 +1640,73 @@ func TestGetSTSLDAPLoginSourceIPIgnoresSpoofedForwardingHeaders(t *testing.T) {
15421640 }
15431641}
15441642
1643+ func TestGetSTSLDAPLoginSourceIPUsesForwardedHeadersForTrustedProxy (t * testing.T ) {
1644+ tests := []struct {
1645+ name string
1646+ headerKey string
1647+ headerValue string
1648+ want string
1649+ }{
1650+ {
1651+ name : "x-forwarded-for" ,
1652+ headerKey : "X-Forwarded-For" ,
1653+ headerValue : "203.0.113.10, 198.51.100.24" ,
1654+ want : "203.0.113.10" ,
1655+ },
1656+ {
1657+ name : "x-real-ip" ,
1658+ headerKey : "X-Real-IP" ,
1659+ headerValue : "203.0.113.10" ,
1660+ want : "203.0.113.10" ,
1661+ },
1662+ {
1663+ name : "forwarded" ,
1664+ headerKey : "Forwarded" ,
1665+ headerValue : `for=203.0.113.10;proto=https` ,
1666+ want : "203.0.113.10" ,
1667+ },
1668+ }
1669+
1670+ withLDAPSTSTrustedProxiesForTest (t , "192.0.2.0/24" , func () {
1671+ for _ , tt := range tests {
1672+ t .Run (tt .name , func (t * testing.T ) {
1673+ req := & http.Request {
1674+ Header : singleHeader (tt .headerKey , tt .headerValue ),
1675+ RemoteAddr : "192.0.2.10:9000" ,
1676+ }
1677+
1678+ if got := getSTSLDAPLoginSourceIP (req ); got != tt .want {
1679+ t .Fatalf ("expected trusted proxy header %s to resolve %q, got %q" , tt .headerKey , tt .want , got )
1680+ }
1681+ })
1682+ }
1683+ })
1684+ }
1685+
1686+ func TestGetSTSLDAPLoginSourceIPTrustedProxyPrefersXRealIPOverXForwardedFor (t * testing.T ) {
1687+ withLDAPSTSTrustedProxiesForTest (t , "192.0.2.0/24" , func () {
1688+ req := & http.Request {
1689+ Header : make (http.Header ),
1690+ RemoteAddr : "192.0.2.10:9000" ,
1691+ }
1692+ req .Header .Set ("X-Forwarded-For" , "198.51.100.99, 203.0.113.10" )
1693+ req .Header .Set ("X-Real-IP" , "203.0.113.10" )
1694+
1695+ if got := getSTSLDAPLoginSourceIP (req ); got != "203.0.113.10" {
1696+ t .Fatalf ("expected trusted proxy path to prefer X-Real-IP over appended X-Forwarded-For, got %q" , got )
1697+ }
1698+ })
1699+ }
1700+
1701+ func TestGetSTSLDAPLoginSourceIPTrustedProxyFallsBackToPeerWithoutForwardingHeaders (t * testing.T ) {
1702+ withLDAPSTSTrustedProxiesForTest (t , "192.0.2.0/24" , func () {
1703+ req := & http.Request {RemoteAddr : "192.0.2.10:9000" }
1704+ if got := getSTSLDAPLoginSourceIP (req ); got != "192.0.2.10" {
1705+ t .Fatalf ("expected trusted proxy path without forwarding headers to fall back to peer address, got %q" , got )
1706+ }
1707+ })
1708+ }
1709+
15451710func TestReserveSTSLDAPLoginUsesPeerAddressBuckets (t * testing.T ) {
15461711 tests := []struct {
15471712 name string
@@ -1574,12 +1739,12 @@ func TestReserveSTSLDAPLoginUsesPeerAddressBuckets(t *testing.T) {
15741739 limiter := newSTSLDAPLoginRateLimiter (time .Hour , 2 , time .Minute )
15751740 withGlobalSTSLDAPLoginRateLimiterForTest (limiter , func () {
15761741 req1 := & http.Request {
1577- Header : http. Header { tt .headerKey : [] string { tt .firstValue }} ,
1742+ Header : singleHeader ( tt .headerKey , tt .firstValue ) ,
15781743 RemoteAddr : "192.0.2.10:9000" ,
15791744 Form : url.Values {stsLDAPUsername : []string {"alice" }},
15801745 }
15811746 req2 := & http.Request {
1582- Header : http. Header { tt .headerKey : [] string { tt .secondValue }} ,
1747+ Header : singleHeader ( tt .headerKey , tt .secondValue ) ,
15831748 RemoteAddr : "192.0.2.10:9001" ,
15841749 Form : url.Values {stsLDAPUsername : []string {"bob" }},
15851750 }
@@ -1609,12 +1774,12 @@ func TestReserveSTSLDAPLoginUsesPeerAddressBuckets(t *testing.T) {
16091774 limiter := newSTSLDAPLoginRateLimiter (time .Hour , 1 , time .Minute )
16101775 withGlobalSTSLDAPLoginRateLimiterForTest (limiter , func () {
16111776 req1 := & http.Request {
1612- Header : http. Header { tt .headerKey : [] string { tt .firstValue }} ,
1777+ Header : singleHeader ( tt .headerKey , tt .firstValue ) ,
16131778 RemoteAddr : "192.0.2.10:9000" ,
16141779 Form : url.Values {stsLDAPUsername : []string {"alice" }},
16151780 }
16161781 req2 := & http.Request {
1617- Header : http. Header { tt .headerKey : [] string { tt .firstValue }} ,
1782+ Header : singleHeader ( tt .headerKey , tt .firstValue ) ,
16181783 RemoteAddr : "192.0.2.11:9000" ,
16191784 Form : url.Values {stsLDAPUsername : []string {"bob" }},
16201785 }
@@ -1645,6 +1810,46 @@ func TestReserveSTSLDAPLoginUsesPeerAddressBuckets(t *testing.T) {
16451810 }
16461811}
16471812
1813+ func TestReserveSTSLDAPLoginUsesForwardedBucketsForTrustedProxy (t * testing.T ) {
1814+ limiter := newSTSLDAPLoginRateLimiter (time .Hour , 1 , time .Minute )
1815+ withGlobalSTSLDAPLoginRateLimiterForTest (limiter , func () {
1816+ withLDAPSTSTrustedProxiesForTest (t , "192.0.2.0/24" , func () {
1817+ req1 := & http.Request {
1818+ Header : singleHeader ("X-Forwarded-For" , "203.0.113.10" ),
1819+ RemoteAddr : "192.0.2.10:9000" ,
1820+ Form : url.Values {stsLDAPUsername : []string {"alice" }},
1821+ }
1822+ req2 := & http.Request {
1823+ Header : singleHeader ("X-Forwarded-For" , "198.51.100.23" ),
1824+ RemoteAddr : "192.0.2.10:9001" ,
1825+ Form : url.Values {stsLDAPUsername : []string {"bob" }},
1826+ }
1827+
1828+ reservation1 := reserveSTSLDAPLogin (req1 )
1829+ if reservation1 == nil {
1830+ t .Fatal ("expected first reservation through trusted proxy to succeed" )
1831+ }
1832+ defer reservation1 .Cancel ()
1833+
1834+ reservation2 := reserveSTSLDAPLogin (req2 )
1835+ if reservation2 == nil {
1836+ t .Fatal ("expected forwarded client IPs behind the same trusted proxy to use distinct source buckets" )
1837+ }
1838+ defer reservation2 .Cancel ()
1839+
1840+ if got := len (limiter .source .entries ); got != 2 {
1841+ t .Fatalf ("expected distinct forwarded client IPs to use two source buckets, got %d" , got )
1842+ }
1843+ if _ , ok := limiter .source .entries ["203.0.113.10" ]; ! ok {
1844+ t .Fatalf ("expected source bucket for forwarded client IP %q, got keys %v" , "203.0.113.10" , limiter .source .entries )
1845+ }
1846+ if _ , ok := limiter .source .entries ["198.51.100.23" ]; ! ok {
1847+ t .Fatalf ("expected source bucket for forwarded client IP %q, got keys %v" , "198.51.100.23" , limiter .source .entries )
1848+ }
1849+ })
1850+ })
1851+ }
1852+
16481853func TestLDAPBindErrorToSTS (t * testing.T ) {
16491854 tests := []struct {
16501855 name string
0 commit comments