Skip to content

Commit 3389976

Browse files
committed
Ratelimit based on JWT claim (#7175)
1 parent 7174a08 commit 3389976

File tree

10 files changed

+235
-2
lines changed

10 files changed

+235
-2
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Rate Limit JWT claim
2+
3+
In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and apply a rate
4+
limit policy using a JWT claim as the key to the rate limit.
5+
6+
## Prerequisites
7+
8+
1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
9+
instructions to deploy the Ingress Controller.
10+
1. Save the public IP address of the Ingress Controller into a shell variable:
11+
12+
```console
13+
IC_IP=XXX.YYY.ZZZ.III
14+
```
15+
16+
1. Save the HTTP port of the Ingress Controller into a shell variable:
17+
18+
```console
19+
IC_HTTP_PORT=<port number>
20+
```
21+
22+
## Step 1 - Deploy a Web Application
23+
24+
Create the application deployment and service:
25+
26+
```console
27+
kubectl apply -f webapp.yaml
28+
```
29+
30+
## Step 2 - Deploy the Rate Limit Policy
31+
32+
In this step, we create a policy with the name `rate-limit-jwt` that allows only 1 request per second coming from a
33+
single IP address.
34+
35+
Create the policy:
36+
37+
```console
38+
kubectl apply -f rate-limit.yaml
39+
```
40+
41+
## Step 3 - Configure Load Balancing
42+
43+
Create a VirtualServer resource for the web application:
44+
45+
```console
46+
kubectl apply -f virtual-server.yaml
47+
```
48+
49+
Note that the VirtualServer references the policy `rate-limit-jwt` created in Step 2.
50+
51+
## Step 4 - Test the Configuration
52+
53+
The JWT payload used in this testing looks like:
54+
55+
```json
56+
{
57+
"name": "Quotation System",
58+
"sub": "quotes",
59+
"iss": "My API Gateway"
60+
}
61+
```
62+
63+
In this test we are relying on the NGINX Plus `ngx_http_auth_jwt_module` to extract the `sub` claim from the JWT payload into the `$jwt_claim_sub` variable and use this as the rate limiting `key`.
64+
65+
Let's test the configuration. If you access the application at a rate that exceeds one request per second, NGINX will
66+
start rejecting your requests:
67+
68+
```console
69+
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "Authorization: Bearer: `cat token.jwt`"
70+
```
71+
72+
```text
73+
Server address: 10.8.1.19:8080
74+
Server name: webapp-dc88fc766-zr7f8
75+
. . .
76+
```
77+
78+
```console
79+
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "Authorization: Bearer: `cat token.jwt`"
80+
```
81+
82+
```text
83+
<html>
84+
<head><title>503 Service Temporarily Unavailable</title></head>
85+
<body>
86+
<center><h1>503 Service Temporarily Unavailable</h1></center>
87+
</body>
88+
</html>
89+
```
90+
91+
> Note: The command result is truncated for the clarity of the example.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: Policy
3+
metadata:
4+
name: rate-limit-jwt
5+
spec:
6+
rateLimit:
7+
rate: 1r/s
8+
key: ${jwt_claim_sub}
9+
zoneSize: 10M
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik15IEFQSSBHYXRld2F5In0.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: webapp
5+
spec:
6+
host: webapp.example.com
7+
policies:
8+
- name: rate-limit-jwt
9+
upstreams:
10+
- name: webapp
11+
service: webapp-svc
12+
port: 80
13+
routes:
14+
- path: /
15+
action:
16+
pass: webapp
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: webapp
5+
spec:
6+
replicas: 1
7+
selector:
8+
matchLabels:
9+
app: webapp
10+
template:
11+
metadata:
12+
labels:
13+
app: webapp
14+
spec:
15+
containers:
16+
- name: webapp
17+
image: nginxdemos/nginx-hello:plain-text
18+
ports:
19+
- containerPort: 8080
20+
---
21+
apiVersion: v1
22+
kind: Service
23+
metadata:
24+
name: webapp-svc
25+
spec:
26+
ports:
27+
- port: 80
28+
targetPort: 8080
29+
protocol: TCP
30+
name: http
31+
selector:
32+
app: webapp

pkg/apis/configuration/validation/policy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ func validateRateLimitZoneSize(zoneSize string, fieldPath *field.Path) field.Err
569569
return allErrs
570570
}
571571

572-
var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_"}
572+
var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_", "jwt_claim_"}
573573

574574
// rateLimitKeyVariables includes NGINX variables allowed to be used in a rateLimit policy key.
575575
var rateLimitKeyVariables = map[string]bool{

site/content/configuration/policy-resource.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ The feature is implemented using the NGINX [ngx_http_limit_req_module](https://n
121121
|Field | Description | Type | Required |
122122
| ---| ---| ---| --- |
123123
|``rate`` | The rate of requests permitted. The rate is specified in requests per second (r/s) or requests per minute (r/m). | ``string`` | Yes |
124-
|``key`` | The key to which the rate limit is applied. Can contain text, variables, or a combination of them. Variables must be surrounded by ``${}``. For example: ``${binary_remote_addr}``. Accepted variables are ``$binary_remote_addr``, ``$request_uri``, ``$url``, ``$http_``, ``$args``, ``$arg_``, ``$cookie_``. | ``string`` | Yes |
124+
|``key`` | The key to which the rate limit is applied. Can contain text, variables, or a combination of them. Variables must be surrounded by ``${}``. For example: ``${binary_remote_addr}``. Accepted variables are ``$binary_remote_addr``, ``$request_uri``, ``$url``, ``$http_``, ``$args``, ``$arg_``, ``$cookie_``, ``$jwt_claim_``. | ``string`` | Yes |
125125
|``zoneSize`` | Size of the shared memory zone. Only positive values are allowed. Allowed suffixes are ``k`` or ``m``, if none are present ``k`` is assumed. | ``string`` | Yes |
126126
|``delay`` | The delay parameter specifies a limit at which excessive requests become delayed. If not set all excessive requests are delayed. | ``int`` | No |
127127
|``noDelay`` | Disables the delaying of excessive requests while requests are being limited. Overrides ``delay`` if both are set. | ``bool`` | No |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: Policy
3+
metadata:
4+
name: rate-limit-jwt-claim-sub
5+
spec:
6+
rateLimit:
7+
rate: 1r/s
8+
key: ${jwt_claim_sub}
9+
zoneSize: 10M
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: virtual-server
5+
spec:
6+
host: virtual-server.example.com
7+
policies:
8+
- name: rate-limit-jwt-claim-sub
9+
upstreams:
10+
- name: backend2
11+
service: backend2-svc
12+
port: 80
13+
- name: backend1
14+
service: backend1-svc
15+
port: 80
16+
routes:
17+
- path: "/backend1"
18+
action:
19+
pass: backend1
20+
- path: "/backend2"
21+
action:
22+
pass: backend2

tests/suite/test_rl_policies.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
rl_vs_override_spec = f"{TEST_DATA}/rate-limit/spec/virtual-server-override.yaml"
2525
rl_vs_override_route = f"{TEST_DATA}/rate-limit/route-subroute/virtual-server-override-route.yaml"
2626
rl_vs_override_spec_route = f"{TEST_DATA}/rate-limit/route-subroute/virtual-server-override-spec-route.yaml"
27+
rl_vs_jwt_claim_sub = f"{TEST_DATA}/rate-limit/spec/virtual-server-jwt-claim-sub.yaml"
28+
rl_pol_jwt_claim_sub = f"{TEST_DATA}/rate-limit/policies/rate-limit-jwt-claim-sub.yaml"
29+
token = f"{TEST_DATA}/jwt-policy/token.jwt"
2730

2831

2932
@pytest.mark.policies
@@ -357,3 +360,53 @@ def test_rl_policy_scaled(
357360
and policy_info["status"]["reason"] == "AddedOrUpdated"
358361
and policy_info["status"]["state"] == "Valid"
359362
)
363+
364+
@pytest.mark.skip_for_nginx_oss
365+
@pytest.mark.parametrize("src", [rl_vs_jwt_claim_sub])
366+
def test_rl_policy_jwt_claim_sub(
367+
self,
368+
kube_apis,
369+
ingress_controller_prerequisites,
370+
crd_ingress_controller,
371+
virtual_server_setup,
372+
test_namespace,
373+
src,
374+
):
375+
"""
376+
Test if rate-limiting policy is working with 1 rps using $jwt_claim_sub as the rate limit key
377+
"""
378+
print(f"Create rl policy")
379+
pol_name = create_policy_from_yaml(kube_apis.custom_objects, rl_pol_jwt_claim_sub, test_namespace)
380+
print(f"Patch vs with policy: {src}")
381+
patch_virtual_server_from_yaml(
382+
kube_apis.custom_objects,
383+
virtual_server_setup.vs_name,
384+
src,
385+
virtual_server_setup.namespace,
386+
)
387+
wait_before_test()
388+
389+
policy_info = read_custom_resource(kube_apis.custom_objects, test_namespace, "policies", pol_name)
390+
occur = []
391+
t_end = time.perf_counter() + 1
392+
resp = requests.get(
393+
virtual_server_setup.backend_1_url,
394+
headers={"host": virtual_server_setup.vs_host, "Authorization": f"Bearer {token}"},
395+
)
396+
print(resp.status_code)
397+
wait_before_test()
398+
assert resp.status_code == 200
399+
while time.perf_counter() < t_end:
400+
resp = requests.get(
401+
virtual_server_setup.backend_1_url,
402+
headers={"host": virtual_server_setup.vs_host, "Authorization": f"Bearer {token}"},
403+
)
404+
occur.append(resp.status_code)
405+
delete_policy(kube_apis.custom_objects, pol_name, test_namespace)
406+
self.restore_default_vs(kube_apis, virtual_server_setup)
407+
assert (
408+
policy_info["status"]
409+
and policy_info["status"]["reason"] == "AddedOrUpdated"
410+
and policy_info["status"]["state"] == "Valid"
411+
)
412+
assert occur.count(200) <= 1

0 commit comments

Comments
 (0)