Skip to content

Commit

Permalink
Ratelimit based on JWT claim (#7175)
Browse files Browse the repository at this point in the history
  • Loading branch information
pdabelf5 authored Jan 23, 2025
1 parent b3dee9d commit bedac28
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 2 deletions.
91 changes: 91 additions & 0 deletions examples/custom-resources/rate-limit-jwt-claim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Rate Limit JWT claim

In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and apply a rate
limit policy using a JWT claim as the key to the rate limit.

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
instructions to deploy the Ingress Controller.
1. Save the public IP address of the Ingress Controller into a shell variable:

```console
IC_IP=XXX.YYY.ZZZ.III
```

1. Save the HTTP port of the Ingress Controller into a shell variable:

```console
IC_HTTP_PORT=<port number>
```

## Step 1 - Deploy a Web Application

Create the application deployment and service:

```console
kubectl apply -f webapp.yaml
```

## Step 2 - Deploy the Rate Limit Policy

In this step, we create a policy with the name `rate-limit-jwt` that allows only 1 request per second coming from a
single IP address.

Create the policy:

```console
kubectl apply -f rate-limit.yaml
```

## Step 3 - Configure Load Balancing

Create a VirtualServer resource for the web application:

```console
kubectl apply -f virtual-server.yaml
```

Note that the VirtualServer references the policy `rate-limit-jwt` created in Step 2.

## Step 4 - Test the Configuration

The JWT payload used in this testing looks like:

```json
{
"name": "Quotation System",
"sub": "quotes",
"iss": "My API Gateway"
}
```

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`.

Let's test the configuration. If you access the application at a rate that exceeds one request per second, NGINX will
start rejecting your requests:

```console
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "Authorization: Bearer: `cat token.jwt`"
```

```text
Server address: 10.8.1.19:8080
Server name: webapp-dc88fc766-zr7f8
. . .
```

```console
curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "Authorization: Bearer: `cat token.jwt`"
```

```text
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
</body>
</html>
```

> Note: The command result is truncated for the clarity of the example.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: rate-limit-jwt
spec:
rateLimit:
rate: 1r/s
key: ${jwt_claim_sub}
zoneSize: 10M
1 change: 1 addition & 0 deletions examples/custom-resources/rate-limit-jwt-claim/token.jwt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik15IEFQSSBHYXRld2F5In0.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
16 changes: 16 additions & 0 deletions examples/custom-resources/rate-limit-jwt-claim/virtual-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: webapp
spec:
host: webapp.example.com
policies:
- name: rate-limit-jwt
upstreams:
- name: webapp
service: webapp-svc
port: 80
routes:
- path: /
action:
pass: webapp
32 changes: 32 additions & 0 deletions examples/custom-resources/rate-limit-jwt-claim/webapp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: webapp-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: webapp
2 changes: 1 addition & 1 deletion pkg/apis/configuration/validation/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ func validateRateLimitZoneSize(zoneSize string, fieldPath *field.Path) field.Err
return allErrs
}

var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_"}
var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_", "jwt_claim_"}

// rateLimitKeyVariables includes NGINX variables allowed to be used in a rateLimit policy key.
var rateLimitKeyVariables = map[string]bool{
Expand Down
2 changes: 1 addition & 1 deletion site/content/configuration/policy-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The feature is implemented using the NGINX [ngx_http_limit_req_module](https://n
|Field | Description | Type | Required |
| ---| ---| ---| --- |
|``rate`` | The rate of requests permitted. The rate is specified in requests per second (r/s) or requests per minute (r/m). | ``string`` | Yes |
|``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 |
|``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 |
|``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 |
|``delay`` | The delay parameter specifies a limit at which excessive requests become delayed. If not set all excessive requests are delayed. | ``int`` | No |
|``noDelay`` | Disables the delaying of excessive requests while requests are being limited. Overrides ``delay`` if both are set. | ``bool`` | No |
Expand Down
9 changes: 9 additions & 0 deletions tests/data/rate-limit/policies/rate-limit-jwt-claim-sub.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: rate-limit-jwt-claim-sub
spec:
rateLimit:
rate: 1r/s
key: ${jwt_claim_sub}
zoneSize: 10M
22 changes: 22 additions & 0 deletions tests/data/rate-limit/spec/virtual-server-jwt-claim-sub.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: virtual-server
spec:
host: virtual-server.example.com
policies:
- name: rate-limit-jwt-claim-sub
upstreams:
- name: backend2
service: backend2-svc
port: 80
- name: backend1
service: backend1-svc
port: 80
routes:
- path: "/backend1"
action:
pass: backend1
- path: "/backend2"
action:
pass: backend2
53 changes: 53 additions & 0 deletions tests/suite/test_rl_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
rl_vs_override_spec = f"{TEST_DATA}/rate-limit/spec/virtual-server-override.yaml"
rl_vs_override_route = f"{TEST_DATA}/rate-limit/route-subroute/virtual-server-override-route.yaml"
rl_vs_override_spec_route = f"{TEST_DATA}/rate-limit/route-subroute/virtual-server-override-spec-route.yaml"
rl_vs_jwt_claim_sub = f"{TEST_DATA}/rate-limit/spec/virtual-server-jwt-claim-sub.yaml"
rl_pol_jwt_claim_sub = f"{TEST_DATA}/rate-limit/policies/rate-limit-jwt-claim-sub.yaml"
token = f"{TEST_DATA}/jwt-policy/token.jwt"


@pytest.mark.policies
Expand Down Expand Up @@ -357,3 +360,53 @@ def test_rl_policy_scaled(
and policy_info["status"]["reason"] == "AddedOrUpdated"
and policy_info["status"]["state"] == "Valid"
)

@pytest.mark.skip_for_nginx_oss
@pytest.mark.parametrize("src", [rl_vs_jwt_claim_sub])
def test_rl_policy_jwt_claim_sub(
self,
kube_apis,
ingress_controller_prerequisites,
crd_ingress_controller,
virtual_server_setup,
test_namespace,
src,
):
"""
Test if rate-limiting policy is working with 1 rps using $jwt_claim_sub as the rate limit key
"""
print(f"Create rl policy")
pol_name = create_policy_from_yaml(kube_apis.custom_objects, rl_pol_jwt_claim_sub, test_namespace)
print(f"Patch vs with policy: {src}")
patch_virtual_server_from_yaml(
kube_apis.custom_objects,
virtual_server_setup.vs_name,
src,
virtual_server_setup.namespace,
)
wait_before_test()

policy_info = read_custom_resource(kube_apis.custom_objects, test_namespace, "policies", pol_name)
occur = []
t_end = time.perf_counter() + 1
resp = requests.get(
virtual_server_setup.backend_1_url,
headers={"host": virtual_server_setup.vs_host, "Authorization": f"Bearer {token}"},
)
print(resp.status_code)
wait_before_test()
assert resp.status_code == 200
while time.perf_counter() < t_end:
resp = requests.get(
virtual_server_setup.backend_1_url,
headers={"host": virtual_server_setup.vs_host, "Authorization": f"Bearer {token}"},
)
occur.append(resp.status_code)
delete_policy(kube_apis.custom_objects, pol_name, test_namespace)
self.restore_default_vs(kube_apis, virtual_server_setup)
assert (
policy_info["status"]
and policy_info["status"]["reason"] == "AddedOrUpdated"
and policy_info["status"]["state"] == "Valid"
)
assert occur.count(200) <= 1

0 comments on commit bedac28

Please sign in to comment.