diff --git a/controllers/nginx/configuration.md b/controllers/nginx/configuration.md index c6d010d9f..101f0ee2d 100644 --- a/controllers/nginx/configuration.md +++ b/controllers/nginx/configuration.md @@ -193,13 +193,15 @@ Please check the [rewrite](/examples/rewrite/nginx/README.md) example. ### Rate limiting -The annotations `ingress.kubernetes.io/limit-connections` and `ingress.kubernetes.io/limit-rps` define a limit on the connections that can be opened by a single client IP address. This can be used to mitigate [DDoS Attacks](https://www.nginx.com/blog/mitigating-ddos-attacks-with-nginx-and-nginx-plus). +The annotations `ingress.kubernetes.io/limit-connections`, `ingress.kubernetes.io/limit-rps`, and `ingress.kubernetes.io/limit-rpm` define a limit on the connections that can be opened by a single client IP address. This can be used to mitigate [DDoS Attacks](https://www.nginx.com/blog/mitigating-ddos-attacks-with-nginx-and-nginx-plus). `ingress.kubernetes.io/limit-connections`: number of concurrent connections allowed from a single IP address. `ingress.kubernetes.io/limit-rps`: number of connections that may be accepted from a given IP each second. -If you specify both annotations in a single Ingress rule, `limit-rps` takes precedence. +`ingress.kubernetes.io/limit-rpm`: number of connections that may be accepted from a given IP each minute. + +If you specify multiple annotations in a single Ingress rule, `limit-rpm`, and then `limit-rps` takes precedence. ### SSL Passthrough diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index 71dc52665..607fb6abc 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -349,6 +349,17 @@ 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, + loc.RateLimit.RPM.Name, + loc.RateLimit.RPM.SharedSize, + loc.RateLimit.RPM.Limit) + if !zones.Has(zone) { + zones.Insert(zone) + } + } + if loc.RateLimit.RPS.Limit > 0 { zone := fmt.Sprintf("limit_req_zone %v zone=%v:%vm rate=%vr/s;", variable, @@ -366,7 +377,7 @@ func buildRateLimitZones(variable string, input interface{}) []string { } // buildRateLimit produces an array of limit_req to be used inside the Path of -// Ingress rules. The order: connections by IP first and RPS next. +// Ingress rules. The order: connections by IP first, then RPS, and RPM last. func buildRateLimit(input interface{}) []string { limits := []string{} @@ -387,6 +398,12 @@ func buildRateLimit(input interface{}) []string { limits = append(limits, limit) } + if loc.RateLimit.RPM.Limit > 0 { + limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;", + loc.RateLimit.RPM.Name, loc.RateLimit.RPM.Burst) + limits = append(limits, limit) + } + return limits } diff --git a/core/pkg/ingress/annotations/ratelimit/main.go b/core/pkg/ingress/annotations/ratelimit/main.go index 4504a7580..fab90860b 100644 --- a/core/pkg/ingress/annotations/ratelimit/main.go +++ b/core/pkg/ingress/annotations/ratelimit/main.go @@ -27,6 +27,7 @@ import ( const ( limitIP = "ingress.kubernetes.io/limit-connections" limitRPS = "ingress.kubernetes.io/limit-rps" + limitRPM = "ingress.kubernetes.io/limit-rpm" // allow 5 times the specified limit as burst defBurst = 5 @@ -45,6 +46,8 @@ type RateLimit struct { Connections Zone `json:"connections"` // RPS indicates a limit with the number of connections per second RPS Zone `json:"rps"` + + RPM Zone `json:"rpm"` } // Equal tests for equality between two RateLimit types @@ -58,6 +61,9 @@ func (rt1 *RateLimit) Equal(rt2 *RateLimit) bool { if !(&rt1.Connections).Equal(&rt2.Connections) { return false } + if !(&rt1.RPM).Equal(&rt2.RPM) { + return false + } if !(&rt1.RPS).Equal(&rt2.RPS) { return false } @@ -111,13 +117,15 @@ func NewParser() parser.IngressAnnotation { // rule used to rewrite the defined paths func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) { + rpm, _ := parser.GetIntAnnotation(limitRPM, ing) rps, _ := parser.GetIntAnnotation(limitRPS, ing) conn, _ := parser.GetIntAnnotation(limitIP, ing) - if rps == 0 && conn == 0 { + if rpm == 0 && rps == 0 && conn == 0 { return &RateLimit{ Connections: Zone{}, RPS: Zone{}, + RPM: Zone{}, }, nil } @@ -136,5 +144,11 @@ func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) { Burst: rps * defBurst, SharedSize: defSharedSize, }, + RPM: Zone{ + Name: fmt.Sprintf("%v_rpm", zoneName), + Limit: rpm, + Burst: rpm * defBurst, + SharedSize: defSharedSize, + }, }, nil } diff --git a/core/pkg/ingress/annotations/ratelimit/main_test.go b/core/pkg/ingress/annotations/ratelimit/main_test.go index 2fd3e1f05..1dab15bfa 100644 --- a/core/pkg/ingress/annotations/ratelimit/main_test.go +++ b/core/pkg/ingress/annotations/ratelimit/main_test.go @@ -75,6 +75,7 @@ func TestBadRateLimiting(t *testing.T) { data := map[string]string{} data[limitIP] = "0" data[limitRPS] = "0" + data[limitRPM] = "0" ing.SetAnnotations(data) _, err := NewParser().Parse(ing) @@ -85,6 +86,7 @@ func TestBadRateLimiting(t *testing.T) { data = map[string]string{} data[limitIP] = "5" data[limitRPS] = "100" + data[limitRPM] = "10" ing.SetAnnotations(data) i, err := NewParser().Parse(ing) @@ -101,4 +103,7 @@ func TestBadRateLimiting(t *testing.T) { if rateLimit.RPS.Limit != 100 { t.Errorf("expected 100 in limit by rps but %v was returend", rateLimit.RPS) } + if rateLimit.RPM.Limit != 10 { + t.Errorf("expected 10 in limit by rpm but %v was returend", rateLimit.RPM) + } } diff --git a/docs/annotations.md b/docs/annotations.md index e63d8976e..513e98d88 100644 --- a/docs/annotations.md +++ b/docs/annotations.md @@ -49,13 +49,14 @@ Key: | Name | Meaning | --- | --- -| `configuration-snippet` | Arbitrary text to put in the generated configuration file. (nginx) -| `enable-cors` | Enable CORS headers in response. (nginx) -| `limit-connections` | Limit concurrent connections per IP address[1]. (nginx) -| `limit-rps` | Limit requests per second per IP address[1]. (nginx) -| `affinity` | Specify a method to stick clients to origins across requests. Found in `nginx`, where the only supported value is `cookie`. (nginx) -| `session-cookie-name` | When `affinity` is set to `cookie`, the name of the cookie to use. (nginx) -| `session-cookie-hash` | When `affinity` is set to `cookie`, the hash algorithm used: `md5`, `sha`, `index`. (nginx) +| `configuration-snippet` | Arbitrary text to put in the generated configuration file. (nginx) +| `enable-cors` | Enable CORS headers in response. (nginx) +| `limit-connections` | Limit concurrent connections per IP address[1]. (nginx) +| `limit-rps` | Limit requests per second per IP address[1]. (nginx) +| `limit-rpm` | Limit requests per minute per IP address. (nginx) +| `affinity` | Specify a method to stick clients to origins across requests. Found in `nginx`, where the only supported value is `cookie`. (nginx) +| `session-cookie-name` | When `affinity` is set to `cookie`, the name of the cookie to use. (nginx) +| `session-cookie-hash` | When `affinity` is set to `cookie`, the hash algorithm used: `md5`, `sha`, `index`. (nginx) | `proxy-body-size` | Maximum request body size. (nginx, haproxy) | `follow-redirects` | Follow HTTP redirects in the response and deliver the redirect target to the client. (trafficserver)