Skip to content

Commit db3392e

Browse files
authored
Always include remoteAddr in source IP list for audit (kubernetes#87167)
* Always include remoteAddr in source IP list for audit Since the remoteAddr is much harder to spoof than headers, always include it in the list of source IPs used in audit logs. * Add v6 tests
1 parent 06b7987 commit db3392e

File tree

2 files changed

+157
-17
lines changed

2 files changed

+157
-17
lines changed

staging/src/k8s.io/apimachinery/pkg/util/net/http.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -206,52 +206,67 @@ func GetHTTPClient(req *http.Request) string {
206206
return "unknown"
207207
}
208208

209-
// SourceIPs splits the comma separated X-Forwarded-For header or returns the X-Real-Ip header or req.RemoteAddr,
210-
// in that order, ignoring invalid IPs. It returns nil if all of these are empty or invalid.
209+
// SourceIPs splits the comma separated X-Forwarded-For header and joins it with
210+
// the X-Real-Ip header and/or req.RemoteAddr, ignoring invalid IPs.
211+
// The X-Real-Ip is omitted if it's already present in the X-Forwarded-For chain.
212+
// The req.RemoteAddr is always the last IP in the returned list.
213+
// It returns nil if all of these are empty or invalid.
211214
func SourceIPs(req *http.Request) []net.IP {
215+
var srcIPs []net.IP
216+
212217
hdr := req.Header
213218
// First check the X-Forwarded-For header for requests via proxy.
214219
hdrForwardedFor := hdr.Get("X-Forwarded-For")
215-
forwardedForIPs := []net.IP{}
216220
if hdrForwardedFor != "" {
217221
// X-Forwarded-For can be a csv of IPs in case of multiple proxies.
218222
// Use the first valid one.
219223
parts := strings.Split(hdrForwardedFor, ",")
220224
for _, part := range parts {
221225
ip := net.ParseIP(strings.TrimSpace(part))
222226
if ip != nil {
223-
forwardedForIPs = append(forwardedForIPs, ip)
227+
srcIPs = append(srcIPs, ip)
224228
}
225229
}
226230
}
227-
if len(forwardedForIPs) > 0 {
228-
return forwardedForIPs
229-
}
230231

231232
// Try the X-Real-Ip header.
232233
hdrRealIp := hdr.Get("X-Real-Ip")
233234
if hdrRealIp != "" {
234235
ip := net.ParseIP(hdrRealIp)
235-
if ip != nil {
236-
return []net.IP{ip}
236+
// Only append the X-Real-Ip if it's not already contained in the X-Forwarded-For chain.
237+
if ip != nil && !containsIP(srcIPs, ip) {
238+
srcIPs = append(srcIPs, ip)
237239
}
238240
}
239241

240-
// Fallback to Remote Address in request, which will give the correct client IP when there is no proxy.
242+
// Always include the request Remote Address as it cannot be easily spoofed.
243+
var remoteIP net.IP
241244
// Remote Address in Go's HTTP server is in the form host:port so we need to split that first.
242245
host, _, err := net.SplitHostPort(req.RemoteAddr)
243246
if err == nil {
244-
if remoteIP := net.ParseIP(host); remoteIP != nil {
245-
return []net.IP{remoteIP}
246-
}
247+
remoteIP = net.ParseIP(host)
247248
}
248-
249249
// Fallback if Remote Address was just IP.
250-
if remoteIP := net.ParseIP(req.RemoteAddr); remoteIP != nil {
251-
return []net.IP{remoteIP}
250+
if remoteIP == nil {
251+
remoteIP = net.ParseIP(req.RemoteAddr)
252+
}
253+
254+
// Don't duplicate remote IP if it's already the last address in the chain.
255+
if remoteIP != nil && (len(srcIPs) == 0 || !remoteIP.Equal(srcIPs[len(srcIPs)-1])) {
256+
srcIPs = append(srcIPs, remoteIP)
252257
}
253258

254-
return nil
259+
return srcIPs
260+
}
261+
262+
// Checks whether the given IP address is contained in the list of IPs.
263+
func containsIP(ips []net.IP, ip net.IP) bool {
264+
for _, v := range ips {
265+
if v.Equal(ip) {
266+
return true
267+
}
268+
}
269+
return false
255270
}
256271

257272
// Extracts and returns the clients IP from the given request.

staging/src/k8s.io/apimachinery/pkg/util/net/http_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,3 +493,128 @@ func TestAllowsHTTP2(t *testing.T) {
493493
})
494494
}
495495
}
496+
497+
func TestSourceIPs(t *testing.T) {
498+
tests := []struct {
499+
name string
500+
realIP string
501+
forwardedFor string
502+
remoteAddr string
503+
expected []string
504+
}{{
505+
name: "no headers, missing remoteAddr",
506+
expected: []string{},
507+
}, {
508+
name: "no headers, just remoteAddr host:port",
509+
remoteAddr: "1.2.3.4:555",
510+
expected: []string{"1.2.3.4"},
511+
}, {
512+
name: "no headers, just remoteAddr host",
513+
remoteAddr: "1.2.3.4",
514+
expected: []string{"1.2.3.4"},
515+
}, {
516+
name: "empty forwarded-for chain",
517+
forwardedFor: " ",
518+
remoteAddr: "1.2.3.4",
519+
expected: []string{"1.2.3.4"},
520+
}, {
521+
name: "invalid forwarded-for chain",
522+
forwardedFor: "garbage garbage values!",
523+
remoteAddr: "1.2.3.4",
524+
expected: []string{"1.2.3.4"},
525+
}, {
526+
name: "partially invalid forwarded-for chain",
527+
forwardedFor: "garbage garbage values!,4.5.6.7",
528+
remoteAddr: "1.2.3.4",
529+
expected: []string{"4.5.6.7", "1.2.3.4"},
530+
}, {
531+
name: "valid forwarded-for chain",
532+
forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7",
533+
remoteAddr: "1.2.3.4",
534+
expected: []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"},
535+
}, {
536+
name: "valid forwarded-for chain with redundant remoteAddr",
537+
forwardedFor: "2.2.2.2,1.2.3.4",
538+
remoteAddr: "1.2.3.4",
539+
expected: []string{"2.2.2.2", "1.2.3.4"},
540+
}, {
541+
name: "invalid Real-Ip",
542+
realIP: "garbage, just garbage!",
543+
remoteAddr: "1.2.3.4",
544+
expected: []string{"1.2.3.4"},
545+
}, {
546+
name: "invalid Real-Ip with forwarded-for",
547+
realIP: "garbage, just garbage!",
548+
forwardedFor: "2.2.2.2",
549+
remoteAddr: "1.2.3.4",
550+
expected: []string{"2.2.2.2", "1.2.3.4"},
551+
}, {
552+
name: "valid Real-Ip",
553+
realIP: "2.2.2.2",
554+
remoteAddr: "1.2.3.4",
555+
expected: []string{"2.2.2.2", "1.2.3.4"},
556+
}, {
557+
name: "redundant Real-Ip",
558+
realIP: "1.2.3.4",
559+
remoteAddr: "1.2.3.4",
560+
expected: []string{"1.2.3.4"},
561+
}, {
562+
name: "valid Real-Ip with forwarded-for",
563+
realIP: "2.2.2.2",
564+
forwardedFor: "120.120.120.126,4.5.6.7",
565+
remoteAddr: "1.2.3.4",
566+
expected: []string{"120.120.120.126", "4.5.6.7", "2.2.2.2", "1.2.3.4"},
567+
}, {
568+
name: "redundant Real-Ip with forwarded-for",
569+
realIP: "2.2.2.2",
570+
forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7",
571+
remoteAddr: "1.2.3.4",
572+
expected: []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"},
573+
}, {
574+
name: "full redundancy",
575+
realIP: "1.2.3.4",
576+
forwardedFor: "1.2.3.4",
577+
remoteAddr: "1.2.3.4",
578+
expected: []string{"1.2.3.4"},
579+
}, {
580+
name: "full ipv6",
581+
realIP: "abcd:ef01:2345:6789:abcd:ef01:2345:6789",
582+
forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,0:1111:2222:3333:4444:5555:6666:7777",
583+
remoteAddr: "aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa",
584+
expected: []string{
585+
"aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111",
586+
"0:1111:2222:3333:4444:5555:6666:7777",
587+
"abcd:ef01:2345:6789:abcd:ef01:2345:6789",
588+
"aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa",
589+
},
590+
}, {
591+
name: "mixed ipv4 ipv6",
592+
forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,1.2.3.4",
593+
remoteAddr: "0:0:0:0:0:ffff:102:304", // ipv6 equivalent to 1.2.3.4
594+
expected: []string{
595+
"aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111",
596+
"1.2.3.4",
597+
},
598+
}}
599+
600+
for _, test := range tests {
601+
t.Run(test.name, func(t *testing.T) {
602+
req, _ := http.NewRequest("GET", "https://cluster.k8s.io/apis/foobars/v1/foo/bar", nil)
603+
req.RemoteAddr = test.remoteAddr
604+
if test.forwardedFor != "" {
605+
req.Header.Set("X-Forwarded-For", test.forwardedFor)
606+
}
607+
if test.realIP != "" {
608+
req.Header.Set("X-Real-Ip", test.realIP)
609+
}
610+
611+
actualIPs := SourceIPs(req)
612+
actual := make([]string, len(actualIPs))
613+
for i, ip := range actualIPs {
614+
actual[i] = ip.String()
615+
}
616+
617+
assert.Equal(t, test.expected, actual)
618+
})
619+
}
620+
}

0 commit comments

Comments
 (0)