diff --git a/controllers/nginx/configuration.md b/controllers/nginx/configuration.md index 90c1fbc534..dc36293afd 100644 --- a/controllers/nginx/configuration.md +++ b/controllers/nginx/configuration.md @@ -207,6 +207,8 @@ The annotations `ingress.kubernetes.io/limit-connections`, `ingress.kubernetes.i `ingress.kubernetes.io/limit-rpm`: number of connections that may be accepted from a given IP each minute. +You can specify the client IP source ranges to be excluded from rate-limiting through the `ingress.kubernetes.io/limit-whitelist` annotation. The value is a comma separated list of CIDRs. + If you specify multiple annotations in a single Ingress rule, `limit-rpm`, and then `limit-rps` takes precedence. The annotation `ingress.kubernetes.io/limit-rate`, `ingress.kubernetes.io/limit-rate-after` define a limit the rate of response transmission to a client. The rate is specified in bytes per second. The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit. @@ -221,7 +223,7 @@ To configure this setting globally for all Ingress rules, the `limit-rate-after` The annotation `ingress.kubernetes.io/ssl-passthrough` allows to configure TLS termination in the pod and not in NGINX. -**Important:** +**Important:** - Using the annotation `ingress.kubernetes.io/ssl-passthrough` invalidates all the other available annotations. This is because SSL Passthrough works in L4 (TCP). - The use of this annotation requires the flag `--enable-ssl-passthrough` (By default it is disabled) diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index b7d3dc860b..0b8c42fbad 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -132,6 +132,7 @@ var ( "buildAuthLocation": buildAuthLocation, "buildAuthResponseHeaders": buildAuthResponseHeaders, "buildProxyPass": buildProxyPass, + "buildWhitelistVariable": buildWhitelistVariable, "buildRateLimitZones": buildRateLimitZones, "buildRateLimit": buildRateLimit, "buildResolvers": buildResolvers, @@ -335,10 +336,23 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string { return defProxyPass } +var ( + whitelistVarMap = map[string]string{} +) + +func buildWhitelistVariable(s string) string { + if _, ok := whitelistVarMap[s]; !ok { + str := base64.URLEncoding.EncodeToString([]byte(s)) + whitelistVarMap[s] = strings.Replace(str, "=", "", -1) + } + + return whitelistVarMap[s] +} + // buildRateLimitZones produces an array of limit_conn_zone in order to allow // rate limiting of request. Each Ingress rule could have up to two zones, one // for connection limit by IP address and other for limiting request per second -func buildRateLimitZones(variable string, input interface{}) []string { +func buildRateLimitZones(input interface{}) []string { zones := sets.String{} servers, ok := input.([]*ingress.Server) @@ -349,9 +363,11 @@ func buildRateLimitZones(variable string, input interface{}) []string { for _, server := range servers { for _, loc := range server.Locations { + whitelistVar := buildWhitelistVariable(loc.RateLimit.Name) + if loc.RateLimit.Connections.Limit > 0 { - zone := fmt.Sprintf("limit_conn_zone %v zone=%v:%vm;", - variable, + zone := fmt.Sprintf("limit_conn_zone $%s_limit zone=%v:%vm;", + whitelistVar, loc.RateLimit.Connections.Name, loc.RateLimit.Connections.SharedSize) if !zones.Has(zone) { @@ -360,8 +376,8 @@ func buildRateLimitZones(variable string, input interface{}) []string { } if loc.RateLimit.RPM.Limit > 0 { - zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/m;", - variable, + zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/m;", + whitelistVar, loc.RateLimit.RPM.Name, loc.RateLimit.RPM.SharedSize, loc.RateLimit.RPM.Limit) @@ -371,8 +387,8 @@ func buildRateLimitZones(variable string, input interface{}) []string { } if loc.RateLimit.RPS.Limit > 0 { - zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/s;", - variable, + zone := fmt.Sprintf("limit_req_zone $%s_limit zone=%v:%vm rate=%vr/s;", + whitelistVar, loc.RateLimit.RPS.Name, loc.RateLimit.RPS.SharedSize, loc.RateLimit.RPS.Limit) diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index 058aaa83d6..9c42638570 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -288,12 +288,24 @@ http { } {{ end }} {{ end }} + {{ if ne $location.RateLimit.Name "" }} + geo ${{ buildWhitelistVariable $location.RateLimit.Name }}_whitelist { + default 0; + {{ range $ip := $location.RateLimit.Whitelist }} + {{ $ip }} 1;{{ end }} + } + + map ${{ buildWhitelistVariable $location.RateLimit.Name }}_whitelist ${{ buildWhitelistVariable $location.RateLimit.Name }}_limit { + 0 {{ $cfg.LimitConnZoneVariable }}; + 1 ""; + } + {{ end }} {{ end }} {{ end }} {{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}} {{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}} - {{ range $zone := (buildRateLimitZones $cfg.LimitConnZoneVariable $servers) }} + {{ range $zone := (buildRateLimitZones $servers) }} {{ $zone }} {{ end }} diff --git a/core/pkg/ingress/annotations/ratelimit/main.go b/core/pkg/ingress/annotations/ratelimit/main.go index acf6a32b0c..2d65753aa3 100644 --- a/core/pkg/ingress/annotations/ratelimit/main.go +++ b/core/pkg/ingress/annotations/ratelimit/main.go @@ -18,11 +18,14 @@ package ratelimit import ( "fmt" + "sort" + "strings" extensions "k8s.io/api/extensions/v1beta1" "k8s.io/ingress/core/pkg/ingress/annotations/parser" "k8s.io/ingress/core/pkg/ingress/resolver" + "k8s.io/ingress/core/pkg/net" ) const ( @@ -31,6 +34,7 @@ const ( limitRPM = "ingress.kubernetes.io/limit-rpm" limitRATE = "ingress.kubernetes.io/limit-rate" limitRATEAFTER = "ingress.kubernetes.io/limit-rate-after" + limitWhitelist = "ingress.kubernetes.io/limit-whitelist" // allow 5 times the specified limit as burst defBurst = 5 @@ -55,6 +59,10 @@ type RateLimit struct { LimitRate int `json:"limit-rate"` LimitRateAfter int `json:"limit-rate-after"` + + Name string `json:"name"` + + Whitelist []string `json:"whitelist"` } // Equal tests for equality between two RateLimit types @@ -80,6 +88,22 @@ func (rt1 *RateLimit) Equal(rt2 *RateLimit) bool { if rt1.LimitRateAfter != rt2.LimitRateAfter { return false } + if rt1.Name != rt2.Name { + return false + } + + for _, r1l := range rt1.Whitelist { + found := false + for _, rl2 := range rt2.Whitelist { + if r1l == rl2 { + found = true + break + } + } + if !found { + return false + } + } return true } @@ -144,6 +168,13 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) { rps, _ := parser.GetIntAnnotation(limitRPS, ing) conn, _ := parser.GetIntAnnotation(limitIP, ing) + val, _ := parser.GetStringAnnotation(limitWhitelist, ing) + + cidrs, err := parseCIDRs(val) + if err != nil { + return nil, err + } + if rpm == 0 && rps == 0 && conn == 0 { return &RateLimit{ Connections: Zone{}, @@ -177,5 +208,33 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) { }, LimitRate: lr, LimitRateAfter: lra, + Name: zoneName, + Whitelist: cidrs, }, nil } + +func parseCIDRs(s string) ([]string, error) { + if s == "" { + return []string{}, nil + } + + values := strings.Split(s, ",") + + ipnets, ips, err := net.ParseIPNets(values...) + if err != nil { + return nil, err + } + + cidrs := []string{} + for k := range ipnets { + cidrs = append(cidrs, k) + } + + for k := range ips { + cidrs = append(cidrs, k) + } + + sort.Strings(cidrs) + + return cidrs, nil +}