Docs

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.0 header.

  • The consumer names johndoe and janedoe and the route ID key-auth-route are 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}"
    done

    Expected: each line ends in 404. A 200 means 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: 1

An invalid key is rejected:

curl -s -i "${GATEWAY_URL}/anything" -H 'apikey: wrong-key' | head -1
HTTP/1.1 401 Unauthorized

Roll 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

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