key-auth
The key-auth plugin supports the use of an authentication key as a mechanism for clients to authenticate themselves before accessing upstream resources.
To use the plugin, you would configure authentication keys on consumers and enable the plugin on routes or services. The key can be included in the request URL query string or request header. APISIX will then verify the key to determine if a request should be allowed or denied to access upstream resources.
When a consumer is successfully authenticated, APISIX adds additional headers, such as X-Consumer-Username, X-Credential-Identifier, and other consumer custom headers if configured, to the request, before proxying it to the upstream service. The upstream service will be able to differentiate between consumers and implement additional logics as needed. If any of these values is not available, the corresponding header will not be added.
When consumers are configured using the Ingress Controller, the consumer name is generated in the format namespace_consumername. As a result, the X-Consumer-Username header will also follow this format instead of just consumername.
Examples
The examples below demonstrate how you can work with the key-auth plugin for different scenarios.
Implement Key Authentication on Route
The following example demonstrates how to implement key authentication on a route and include the key in the request header.
- Admin API
- ADC
- Ingress Controller
Create a consumer jack:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jack"
}'
Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jack-key-auth",
"plugins": {
"key-auth": {
"key": "jack-key"
}
}
}'
Create a route with key-auth:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "key-auth-route",
"uri": "/anything",
"plugins": {
"key-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
consumers:
- username: jack
credentials:
- name: key-auth
type: key-auth
config:
key: jack-key
services:
- name: key-auth-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth: {}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
- Gateway API
- APISIX CRD
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jack
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-cred
config:
key: jack-key
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: key-auth-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: key-auth-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: key-auth-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jack
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: jack-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: key-auth-route
spec:
ingressClassName: apisix
http:
- name: key-auth-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
Verify with a Valid Key
Send a request to the route with the valid key:
curl -i "http://127.0.0.1:9080/anything" -H 'apikey: jack-key'
You should receive an HTTP/1.1 200 OK response.
Verify with an Invalid Key
Send a request with an invalid key:
curl -i "http://127.0.0.1:9080/anything" -H 'apikey: wrong-key'
You should see an HTTP/1.1 401 Unauthorized response with the following:
{"message":"Invalid API key in request"}
Verify without a Key
Send a request to the route without a key:
curl -i "http://127.0.0.1:9080/anything"
You should see an HTTP/1.1 401 Unauthorized response with the following:
{"message":"Missing API key found in request"}
Hide Authentication Information From Upstream
The following example first demonstrates the default behavior, where the authentication key is forwarded to the upstream services, and then shows how to prevent the key from being sent by configuring hide_credentials. Forwarding the authentication key to upstream services might lead to security risks in some circumstances.
- Admin API
- ADC
- Ingress Controller
Create a consumer jack:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jack"
}'
Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jack-key-auth",
"plugins": {
"key-auth": {
"key": "jack-key"
}
}
}'
Without Hiding Credentials
Create a route with key-auth and configure hide_credentials to false, which is the default configuration:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "key-auth-route",
"uri": "/anything",
"plugins": {
"key-auth": {
"hide_credentials": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
consumers:
- username: jack
credentials:
- name: key-auth
type: key-auth
config:
key: jack-key
services:
- name: key-auth-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth:
hide_credentials: false
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
- Gateway API
- APISIX CRD
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jack
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-cred
config:
key: jack-key
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: key-auth-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
hide_credentials: false
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: key-auth-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: key-auth-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jack
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: jack-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: key-auth-route
spec:
ingressClassName: apisix
http:
- name: key-auth-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
config:
hide_credentials: false
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
Send a request with the valid key:
curl -i "http://127.0.0.1:9080/anything?apikey=jack-key"
You should see an HTTP/1.1 200 OK response with the following:
{
"args": {
"apikey": "jack-key"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.2.1",
"X-Consumer-Username": "jack",
"X-Credential-Identifier": "cred-jack-key-auth",
"X-Amzn-Trace-Id": "Root=1-6502d8a5-2194962a67aa21dd33f94bb2",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"method": "GET",
"origin": "127.0.0.1, 103.248.35.179",
"url": "http://127.0.0.1/anything?apikey=jack-key"
}
Note that the credential jack-key is visible to the upstream service.
Hide Credentials
- Admin API
- ADC
- Ingress Controller
Update the plugin's hide_credentials to true:
curl "http://127.0.0.1:9180/apisix/admin/routes/key-auth-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"key-auth": {
"hide_credentials": true
}
}
}'
Update the route configuration:
services:
- name: key-auth-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth:
hide_credentials: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Update the PluginConfig to set hide_credentials to true:
# other configs
# ---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: key-auth-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
hide_credentials: true
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
Update the ApisixRoute to set hide_credentials to true:
# other configs
# ---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: key-auth-route
spec:
ingressClassName: apisix
http:
- name: key-auth-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
config:
hide_credentials: true
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
Send a request with the valid key:
curl -i "http://127.0.0.1:9080/anything?apikey=jack-key"
You should see an HTTP/1.1 200 OK response with the following:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.2.1",
"X-Consumer-Username": "jack",
"X-Credential-Identifier": "cred-jack-key-auth",
"X-Amzn-Trace-Id": "Root=1-6502d85c-16f34dbb5629a5960183e803",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"method": "GET",
"origin": "127.0.0.1, 103.248.35.179",
"url": "http://127.0.0.1/anything"
}
Note that the credential jack-key is no longer visible to the upstream service.
Demonstrate Priority of Keys in Header and Query
The following example demonstrates how to implement key authentication by consumers on a route and customize the URL parameter that should include the key. The example also shows that when the API key is configured in both the header and the query string, the request header has a higher priority.
- Admin API
- ADC
- Ingress Controller
Create a consumer jack:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jack"
}'
Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jack-key-auth",
"plugins": {
"key-auth": {
"key": "jack-key"
}
}
}'
Create a route with key-auth:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "key-auth-route",
"uri": "/anything",
"plugins": {
"key-auth": {
"query": "auth"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
consumers:
- username: jack
credentials:
- name: key-auth
type: key-auth
config:
key: jack-key
services:
- name: key-auth-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth:
query: auth
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Create a consumer with key-auth credential and a route with key-auth plugin configured as such:
- Gateway API
- APISIX CRD
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jack
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-cred
config:
key: jack-key
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: key-auth-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
query: auth
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: key-auth-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: key-auth-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jack
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: jack-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: key-auth-route
spec:
ingressClassName: apisix
http:
- name: key-auth-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
enable: true
config:
query: auth
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
Verify with a Valid Key
Send a request to the route with the valid key:
curl -i "http://127.0.0.1:9080/anything?auth=jack-key"
You should receive an HTTP/1.1 200 OK response.
Verify with an Invalid Key
Send a request with an invalid key:
curl -i "http://127.0.0.1:9080/anything?auth=wrong-key"
You should see an HTTP/1.1 401 Unauthorized response with the following:
{"message":"Invalid API key in request"}
Verify with a Valid Key in Query String
However, if you include the valid key in header with the invalid key still in the URL query string:
curl -i "http://127.0.0.1:9080/anything?auth=wrong-key" -H 'apikey: jack-key'
You should see an HTTP/1.1 200 OK response. This shows that the key included in the header always has a higher priority.
Add Consumer Custom ID to Header
The following example demonstrates how you can attach a consumer custom ID to authenticated request in the Consumer-Custom-Id header, which can be used to implement additional logics as needed.
- Admin API
- ADC
- Ingress Controller
Create a consumer jack with a custom ID label:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jack",
"labels": {
"custom_id": "495aec6a"
}
}'
Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jack-key-auth",
"plugins": {
"key-auth": {
"key": "jack-key"
}
}
}'
Create a route with key-auth:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "key-auth-route",
"uri": "/anything",
"plugins": {
"key-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a consumer with key-auth credential and a route with key-auth plugin enabled:
consumers:
- username: jack
labels:
custom_id: "495aec6a"
credentials:
- name: key-auth
type: key-auth
config:
key: jack-key
services:
- name: key-auth-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth: {}
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
Consumer custom labels are currently not supported when configuring resources through the Ingress Controller, and the X-Consumer-Custom-Id header is not included in requests. At the moment, this example cannot be completed with the Ingress Controller.
To verify, send a request to the route with the valid key:
curl -i "http://127.0.0.1:9080/anything?apikey=jack-key"
You should see an HTTP/1.1 200 OK response similar to the following:
{
"args": {
"apikey": "jack-key"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-66ea8d64-33df89052ae198a706e18c2a",
"X-Consumer-Username": "jack",
"X-Credential-Identifier": "cred-jack-key-auth",
"X-Consumer-Custom-Id": "495aec6a",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"method": "GET",
"origin": "192.168.65.1, 205.198.122.37",
"url": "http://127.0.0.1/anything?apikey=jack-key"
}
If you would like to attach more consumer custom headers to authenticated requests, see the attach-consumer-label plugin.
Rate Limit with Anonymous Consumer
The following example demonstrates how you can configure different rate limiting policies by regular and anonymous consumers, where the anonymous consumer does not need to authenticate and has less quota.
- Admin API
- ADC
- Ingress Controller
Create a regular consumer jack and configure the limit-count plugin to allow for a quota of 3 within a 30-second window:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jack",
"plugins": {
"limit-count": {
"count": 3,
"time_window": 30,
"rejected_code": 429,
"policy": "local"
}
}
}'
Create the key-auth credential for the consumer jack:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jack-key-auth",
"plugins": {
"key-auth": {
"key": "jack-key"
}
}
}'
Create an anonymous user anonymous and configure the limit-count plugin to allow for a quota of 1 within a 30-second window:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "anonymous",
"plugins": {
"limit-count": {
"count": 1,
"time_window": 30,
"rejected_code": 429,
"policy": "local"
}
}
}'
Create a route and configure the key-auth plugin to accept anonymous consumer anonymous from bypassing the authentication:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "key-auth-route",
"uri": "/anything",
"plugins": {
"key-auth": {
"anonymous_consumer": "anonymous"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Configure consumers with different rate limits and a route that accepts anonymous users:
consumers:
- username: jack
plugins:
limit-count:
count: 3
time_window: 30
rejected_code: 429
policy: local
credentials:
- name: key-auth
type: key-auth
config:
key: jack-key
- username: anonymous
plugins:
limit-count:
count: 1
time_window: 30
rejected_code: 429
policy: local
services:
- name: anonymous-rate-limit-service
routes:
- name: key-auth-route
uris:
- /anything
plugins:
key-auth:
anonymous_consumer: anonymous
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
Configure consumers with different rate limits and a route that accepts anonymous users:
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jack
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: jack-key
plugins:
- name: limit-count
config:
count: 3
time_window: 30
rejected_code: 429
policy: local
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: anonymous
spec:
gatewayRef:
name: apisix
plugins:
- name: limit-count
config:
count: 1
time_window: 30
rejected_code: 429
policy: local
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: key-auth-plugin-config
spec:
plugins:
- name: key-auth
config:
anonymous_consumer: aic_anonymous # namespace_consumername
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: key-auth-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: key-auth-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
Apply the configuration to your cluster:
kubectl apply -f key-auth-ic.yaml
The ApisixConsumer CRD currently does not support configuring plugins on consumers, except for the authentication plugins allowed in authParameter. This example cannot be completed with APISIX CRDs.
To verify, send five consecutive requests with jack's key:
resp=$(seq 5 | xargs -I{} curl "http://127.0.0.1:9080/anything" -H 'apikey: jack-key' -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429
You should see the following response, showing that out of the 5 requests, 3 requests were successful (status code 200) while the others were rejected (status code 429).
200: 3, 429: 2
Send five anonymous requests:
resp=$(seq 5 | xargs -I{} curl "http://127.0.0.1:9080/anything" -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429
You should see the following response, showing that only one request was successful:
200: 1, 429: 4