Implement Key Authentication
Learn how to set up key authentication in Apache APISIX, allowing you to issue unique API keys to consumers for effective access control to your APIs.
Applies to:Apache APISIX 3.18.0
Roles:Platform engineerTime:10 minutesImpact:No restart, no traffic change
Goal
A route that authenticates two consumers by API key, forwards their consumer identities upstream, and applies a different request quota to each consumer.
API keys are suitable when the gateway only needs to identify a caller; public-facing or highly sensitive APIs can require a stronger authentication protocol.
Before you begin
- You have an Apache APISIX installation, cURL, and jq. Get APISIX installs one in Docker or Kubernetes.
Set the variables this guide uses:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo-
The Admin API accepts your key. Check it with:
curl -s -o /dev/null -w '%{http_code}\n' "${APISIX_ADMIN_URL}/apisix/admin/routes" \ -H "X-API-KEY: ${APISIX_ADMIN_KEY}"Expected:
200. -
The data plane is reachable. Check it with:
curl -sI "${GATEWAY_URL}" | grep -i '^server'Expected: a
Server: APISIX/3.18.0header. -
The consumer names
johndoeandjanedoeand the route IDkey-auth-routeare unused. Check all three with:for path in consumers/johndoe consumers/janedoe routes/key-auth-route; do curl -s -o /dev/null -w "${path}: %{http_code}\n" \ "${APISIX_ADMIN_URL}/apisix/admin/${path}" \ -H "X-API-KEY: ${APISIX_ADMIN_KEY}" doneExpected: each line ends in
404. A200means that identifier already exists; stop and use unique names consistently rather than overwriting it.
Steps
Three steps create the consumer identities and quotas, attach API-key credentials, and expose an authenticated route.
Step 1: Create two consumers with different quotas
Create one consumer allowed one request per 30 seconds and another allowed two.
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"labels": {"custom_id": "john-doe-junior"},
"plugins": {
"limit-count": {"count": 1, "time_window": 30, "rejected_code": 429}
}
}'
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "janedoe",
"labels": {"custom_id": "jane-doe-senior"},
"plugins": {
"limit-count": {"count": 2, "time_window": 30, "rejected_code": 429}
}
}'{ "value": { "username": "johndoe", "plugins": { "limit-count": { "count": 1, "time_window": 30, … } } } }
{ "value": { "username": "janedoe", "plugins": { "limit-count": { "count": 2, "time_window": 30, … } } } }What changed: two consumer identities now carry separate local rate limits, but neither can authenticate yet.
Step 2: Create an API-key credential for each consumer
Attach one unique key-auth credential to each consumer.
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers/johndoe/credentials" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"id": "cred-john-key-auth",
"plugins": {"key-auth": {"key": "john-key"}}
}'
curl -s "${APISIX_ADMIN_URL}/apisix/admin/consumers/janedoe/credentials" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"id": "cred-jane-key-auth",
"plugins": {"key-auth": {"key": "jane-key"}}
}'{ "value": { "id": "cred-john-key-auth", "plugins": { "key-auth": { "key": "john-key" } } } }
{ "value": { "id": "cred-jane-key-auth", "plugins": { "key-auth": { "key": "jane-key" } } } }What changed: each consumer can now be resolved from its API key.
Step 3: Create an authenticated route
Expose the upstream at /anything and require key-auth on that route.
curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/key-auth-route" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/anything",
"plugins": {"key-auth": {}},
"upstream": {
"type": "roundrobin",
"nodes": {"httpbin.org:80": 1}
}
}'{ "value": { "id": "key-auth-route", "uri": "/anything", "plugins": { "key-auth": {} }, … } }What changed: /anything now rejects requests that do not contain a valid API key.
Validate
Send a request as johndoe and inspect the identity forwarded to the upstream:
curl -s "${GATEWAY_URL}/anything" -H 'apikey: john-key' | \
jq '{username: .headers["X-Consumer-Username"], credential: .headers["X-Credential-Identifier"], custom_id: .headers["X-Consumer-Custom-Id"]}'{
"username": "johndoe",
"credential": "cred-john-key-auth",
"custom_id": "john-doe-junior"
}After the current 30-second window expires, send three requests as each consumer and compare their quotas:
resp=$(seq 3 | xargs -I{} curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/anything" -H 'apikey: john-key') && \
echo "johndoe — 200: $(echo "$resp" | grep -c 200), 429: $(echo "$resp" | grep -c 429)"
resp=$(seq 3 | xargs -I{} curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/anything" -H 'apikey: jane-key') && \
echo "janedoe — 200: $(echo "$resp" | grep -c 200), 429: $(echo "$resp" | grep -c 429)"johndoe — 200: 1, 429: 2
janedoe — 200: 2, 429: 1An invalid key is rejected:
curl -s -i "${GATEWAY_URL}/anything" -H 'apikey: wrong-key' | head -1HTTP/1.1 401 UnauthorizedRoll back / Clean up
Remove the route first, then the credentials and consumers it depended on:
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/key-auth-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/johndoe/credentials/cred-john-key-auth" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/janedoe/credentials/cred-jane-key-auth" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/johndoe" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/janedoe" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"The first command takes this guide's /anything route offline. Each successful deletion returns 200; another route can still match the same path.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
A request returns 401 with Invalid API key in request. | The key does not match a key-auth credential, or the credential has not reached the gateway yet. | Compare the apikey header with the credential created in step 2, wait a few seconds, and retry. |
A request returns 401 with Missing API key found in request. | The request does not contain the header where key-auth looks for the key. | Send the key in the apikey header, or configure the plugin's header or query field explicitly. |
| More requests succeed than the configured quota. | limit-count uses the local policy by default, so each APISIX instance keeps its own counter. | Use a Redis-backed policy (opens in Plugin Hub docs) when multiple gateway instances must share one counter. |
Related
- Consumers — how APISIX associates plugins and credentials with an API consumer.
- Credentials — how credentials belong to consumers.
key-authplugin reference (opens in Plugin Hub docs) — header, query-string, anonymous-consumer, and response behavior.limit-countplugin reference (opens in Plugin Hub docs) — quota keys, counter policies, and rejected responses.- Implement Basic Authentication — authenticate a consumer with a username and password.