Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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
Loading