Authenticated Rate Limiting for gRPC Services¶
This guide walks through configuring authenticated rate limiting for a gRPC service using Kuadrant's RateLimitPolicy and AuthPolicy attached to a GRPCRoute. It demonstrates:
- Service-level rate limiting using a CEL predicate on the whole GRPCRoute
- Method-level overrides using GRPCRoute section name targeting
- Per-method authentication combined with rate limiting on a specific GRPCRoute section
Prerequisites¶
- Kubernetes cluster with the Kuadrant operator installed, and an instance of Kuadrant running in the cluster. See our Getting Started guide for more information.
- kubectl command line tool.
- grpcurl for testing gRPC endpoints.
Setup environment variables¶
Set the following environment variables used for convenience in this guide:
export KUADRANT_GATEWAY_NS=api-gateway # Namespace for the example Gateway
export KUADRANT_GATEWAY_NAME=external # Name for the example Gateway
export KUADRANT_GRPC_NS=grpcbin # Namespace for the example gRPC app
Create an Ingress Gateway¶
Create the namespace the Gateway will be deployed in:
Create a gateway:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: ${KUADRANT_GATEWAY_NAME}
namespace: ${KUADRANT_GATEWAY_NS}
labels:
kuadrant.io/gateway: "true"
spec:
gatewayClassName: istio
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
EOF
Note: The
Gatewayresource created above uses Istio as the gateway provider. For Envoy Gateway, use the Envoy GatewayGatewayClassas thegatewayClassName.
Check the status of the Gateway ensuring the gateway is Accepted and Programmed:
kubectl get gateway ${KUADRANT_GATEWAY_NAME} -n ${KUADRANT_GATEWAY_NS} -o=jsonpath='{.status.conditions[?(@.type=="Accepted")].message}{"\n"}{.status.conditions[?(@.type=="Programmed")].message}{"\n"}'
Deploy the gRPC backend¶
Create the namespace for the gRPC application:
Deploy the grpcbin echo service — a gRPC equivalent of httpbin with built-in reflection support:
kubectl apply -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/main/examples/grpc-backend/grpcbin.yaml -n ${KUADRANT_GRPC_NS}
Verify the deployment is ready:
Create a GRPCRoute with named sections¶
Create a GRPCRoute with named rules that use GRPCRoute matches to separate traffic by method. Each rule uses a GRPCMethodMatch to match a specific service and method:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: grpcbin
namespace: ${KUADRANT_GRPC_NS}
spec:
parentRefs:
- name: ${KUADRANT_GATEWAY_NAME}
namespace: ${KUADRANT_GATEWAY_NS}
hostnames: ["grpcbin.local"]
rules:
- name: unary-methods
matches:
- method:
service: grpcbin.GRPCBin
method: DummyUnary
backendRefs:
- name: grpcbin
port: 9000
- name: header-methods
matches:
- method:
service: grpcbin.GRPCBin
method: HeadersUnary
backendRefs:
- name: grpcbin
port: 9000
- name: other
backendRefs:
- name: grpcbin
port: 9000
EOF
This creates three rules:
unary-methods— matchesgrpcbin.GRPCBin/DummyUnaryheader-methods— matchesgrpcbin.GRPCBin/HeadersUnaryother— catch-all for remaining traffic (including gRPC reflection, used by tools likegrpcurl)
The named sections can be referenced by policies for fine-grained targeting.
Verify the route¶
Export the gateway address and test both methods:
export KUADRANT_INGRESS_HOST=$(kubectl get gtw ${KUADRANT_GATEWAY_NAME} -n ${KUADRANT_GATEWAY_NS} -o jsonpath='{.status.addresses[0].value}')
export KUADRANT_INGRESS_PORT=$(kubectl get gtw ${KUADRANT_GATEWAY_NAME} -n ${KUADRANT_GATEWAY_NS} -o jsonpath='{.spec.listeners[?(@.name=="http")].port}')
export KUADRANT_GATEWAY_URL=${KUADRANT_INGRESS_HOST}:${KUADRANT_INGRESS_PORT}
Note: If the gateway address is not available, try forwarding requests to the service:
Test DummyUnary:
grpcurl -plaintext -authority grpcbin.local -d '{"f_string":"hello"}' $KUADRANT_GATEWAY_URL grpcbin.GRPCBin/DummyUnary
Expected:
Test HeadersUnary:
Expected: a response containing request metadata headers.
Step 1: Service-level rate limiting with CEL predicates¶
Apply a RateLimitPolicy to the entire GRPCRoute using a CEL predicate to match all grpcbin traffic. This sets a default rate limit of 10 requests per 10 seconds across all methods:
kubectl apply -f - <<EOF
apiVersion: kuadrant.io/v1
kind: RateLimitPolicy
metadata:
name: grpcbin-rlp
namespace: ${KUADRANT_GRPC_NS}
spec:
targetRef:
group: gateway.networking.k8s.io
kind: GRPCRoute
name: grpcbin
defaults:
limits:
"grpcbin-service":
rates:
- limit: 10
window: 10s
when:
- predicate: "request.url_path.startsWith('/grpcbin.GRPCBin/')"
EOF
The when predicate ensures only calls to the grpcbin.GRPCBin service are rate limited. gRPC reflection calls (used by tools like grpcurl) are excluded, making test results predictable.
Note: It may take a couple of minutes for the RateLimitPolicy to be applied depending on your cluster.
Verify the policy is accepted:
Wait for Accepted and Enforced conditions to be True.
Test the service-level limit¶
Both methods share the same 10 requests per 10 second limit:
for i in {1..12}; do
echo "Request $i:"
grpcurl -plaintext -authority grpcbin.local \
-d '{"f_string": "hello"}' \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/DummyUnary
sleep 0.5
done
Expected: 10 successful responses, then Code: Unavailable (rate limited) for requests 11-12.
Step 2: Method-specific override with section name targeting¶
With the service-level policy still in place, add a second RateLimitPolicy that targets a specific GRPCRoute section. This overrides the default for that section only:
kubectl apply -f - <<EOF
apiVersion: kuadrant.io/v1
kind: RateLimitPolicy
metadata:
name: grpcbin-unary-rlp
namespace: ${KUADRANT_GRPC_NS}
spec:
targetRef:
group: gateway.networking.k8s.io
kind: GRPCRoute
name: grpcbin
sectionName: unary-methods
limits:
"unary-limit":
rates:
- limit: 5
window: 10s
EOF
Now the rate limits are:
| Method | Rate Limit | Source |
|---|---|---|
| DummyUnary | 5 per 10s | Section-level override (grpcbin-unary-rlp) |
| HeadersUnary | 10 per 10s | Inherited from route-level default (grpcbin-rlp) |
Test the override¶
Test DummyUnary (5rp10s override):
for i in {1..8}; do
echo "Request $i:"
grpcurl -plaintext -authority grpcbin.local \
-d '{"f_string": "hello"}' \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/DummyUnary
sleep 1
done
Expected: 5 successful responses, then Code: Unavailable from request 6 onwards.
Test HeadersUnary (10rp10s default):
for i in {1..12}; do
echo "Request $i:"
grpcurl -plaintext -authority grpcbin.local \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/HeadersUnary
sleep 0.5
done
Expected: 10 successful responses, then Code: Unavailable for requests 11-12. The route-level default still applies.
Step 3: Add authentication to a specific section¶
Apply an AuthPolicy targeting only the unary-methods section. DummyUnary will now require an API key, while HeadersUnary remains open:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: grpcbin-apikey
namespace: ${KUADRANT_GRPC_NS}
labels:
authorino.kuadrant.io/managed-by: authorino
app: grpcbin
annotations:
secret.kuadrant.io/user-id: grpcuser
stringData:
api_key: GRPCBINKEY123
type: Opaque
---
apiVersion: kuadrant.io/v1
kind: AuthPolicy
metadata:
name: grpcbin-auth
namespace: ${KUADRANT_GRPC_NS}
spec:
targetRef:
group: gateway.networking.k8s.io
kind: GRPCRoute
name: grpcbin
sectionName: unary-methods
rules:
authentication:
"api-key":
apiKey:
selector:
matchLabels:
app: grpcbin
allNamespaces: true
credentials:
authorizationHeader:
prefix: APIKEY
EOF
Now the policies in effect are:
| Method | Rate Limit | Authentication |
|---|---|---|
| DummyUnary | 5 per 10s (section override) | API key required |
| HeadersUnary | 10 per 10s (route default) | None |
Test the combined setup¶
DummyUnary without API key — rejected (401):
grpcurl -plaintext -authority grpcbin.local \
-d '{"f_string": "hello"}' \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/DummyUnary
Expected:
Note: Wait 10 seconds before the next test to ensure the rate limit counter from the unauthenticated request has reset.
DummyUnary with API key — accepted and rate limited (5rp10s):
for i in {1..8}; do
echo "Request $i:"
grpcurl -plaintext -authority grpcbin.local \
-H 'Authorization: APIKEY GRPCBINKEY123' \
-d '{"f_string": "hello"}' \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/DummyUnary
sleep 1
done
Expected: 5 successful responses, then Code: Unavailable from request 6 onwards.
HeadersUnary without API key — accepted and rate limited (10rp10s):
for i in {1..12}; do
echo "Request $i:"
grpcurl -plaintext -authority grpcbin.local \
$KUADRANT_GATEWAY_URL grpcbin.GRPCBin/HeadersUnary
sleep 0.5
done
Expected: 10 successful responses, then Code: Unavailable. No authentication required — the AuthPolicy only targets unary-methods.
Cleanup¶
kubectl delete authpolicy grpcbin-auth -n ${KUADRANT_GRPC_NS}
kubectl delete ratelimitpolicy grpcbin-unary-rlp -n ${KUADRANT_GRPC_NS}
kubectl delete ratelimitpolicy grpcbin-rlp -n ${KUADRANT_GRPC_NS}
kubectl delete secret grpcbin-apikey -n ${KUADRANT_GRPC_NS}
kubectl delete grpcroute grpcbin -n ${KUADRANT_GRPC_NS}
kubectl delete -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/main/examples/grpc-backend/grpcbin.yaml -n ${KUADRANT_GRPC_NS}
kubectl delete ns ${KUADRANT_GRPC_NS}
kubectl delete gateway ${KUADRANT_GATEWAY_NAME} -n ${KUADRANT_GATEWAY_NS}
kubectl delete ns ${KUADRANT_GATEWAY_NS}