Docs
Apache APISIXSecuritySecurity HardeningSecrets ManagementManage APISIX Credentials in HashiCorp Vault

Manage APISIX Credentials in HashiCorp Vault

Store a generated key-auth credential in a disposable HashiCorp Vault server, give Apache APISIX a short-lived read-only token, validate authentication, and clean up every test resource.

Applies to:Apache APISIX 3.18.0

Deployments:DockerRoles:Platform engineerTime:20 minutesImpact:Changes live traffic

Goal

Run an isolated Vault development server with a KV v1 secrets engine, create a one-hour token that can read only one secret path, and configure APISIX to resolve a key-auth credential from that path. Prove valid and invalid requests against a controlled upstream.

Vault development mode stores data in memory, starts unsealed, and is not for production. This guide keeps Vault on the APISIX quickstart network without publishing its port. Use a durable, TLS-protected Vault deployment and an approved workload-authentication method in production.

Before you begin

  • Complete Get APISIX with the Docker quickstart.
  • Install Docker, cURL, jq, and OpenSSL.
  • Use a shell that supports Bash or Zsh syntax.

Set the APISIX endpoints and read the existing Admin API key without placing it in shell history:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo

Protect the fixed APISIX resources, containers, and local files used by this guide:

test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/secrets/vault/vault-secret-guide" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: APISIX secret vault/vault-secret-guide already exists'; exit 1; }
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/consumers/vault-secret-guide" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: consumer vault-secret-guide already exists'; exit 1; }
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/vault-secret-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route vault-secret-guide-route already exists'; exit 1; }
test -z "$(docker ps -aq --filter name=^/apisix-secret-guide-vault$)" || { echo 'Stop: container apisix-secret-guide-vault already exists'; exit 1; }
test -z "$(docker ps -aq --filter name=^/vault-secret-guide-upstream$)" || { echo 'Stop: container vault-secret-guide-upstream already exists'; exit 1; }
for file in vault-secret-guide-root-token.txt vault-secret-guide-root.env vault-secret-guide-key.txt vault-secret-guide-token.json vault-secret-guide-body.txt; do
  test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }
done

Steps

Step 1: Start an isolated development Vault

Generate a root token without printing it and put it in a permission-restricted Docker environment file:

umask 077
openssl rand -hex 24 > vault-secret-guide-root-token.txt
export VAULT_ROOT_TOKEN="$(cat vault-secret-guide-root-token.txt)"
printf 'VAULT_DEV_ROOT_TOKEN_ID=%s\nVAULT_TOKEN=%s\nVAULT_ADDR=http://127.0.0.1:8200\n' \
  "${VAULT_ROOT_TOKEN}" "${VAULT_ROOT_TOKEN}" > vault-secret-guide-root.env
unset VAULT_ROOT_TOKEN

Start the pinned Vault image only on the existing quickstart network and wait for it to become ready:

docker run -d --name apisix-secret-guide-vault \
  --cap-add=IPC_LOCK \
  --network apisix-quickstart-net \
  --env-file vault-secret-guide-root.env \
  hashicorp/vault:1.21.4 server -dev
for attempt in {1..15}; do
  docker exec apisix-secret-guide-vault vault status >/dev/null 2>&1 && break
  sleep 1
done
docker exec apisix-secret-guide-vault vault status >/dev/null

The generated root token is confined to this disposable development container and its protected local files. Do not use a root token or development mode outside an isolated test.

What changed: one in-memory Vault server is reachable from the APISIX container but is not published to the host network.

Step 2: Create the secret and a scoped token

Enable a dedicated KV v1 engine, generate the authentication key, and pass its value to Vault through standard input:

docker exec apisix-secret-guide-vault vault secrets enable -path=apisix-kv -version=1 kv
umask 077
openssl rand -hex 24 > vault-secret-guide-key.txt
docker exec -i apisix-secret-guide-vault vault kv put -mount=apisix-kv consumer key-auth=- \
  < vault-secret-guide-key.txt >/dev/null

Create a policy for the exact KV v1 path, then write a non-renewable one-hour child token to a protected JSON file:

printf '%s\n' \
  'path "apisix-kv/consumer" {' \
  '  capabilities = ["read"]' \
  '}' | docker exec -i apisix-secret-guide-vault vault policy write apisix-secret-guide -
docker exec apisix-secret-guide-vault vault token create \
  -policy=apisix-secret-guide \
  -ttl=1h \
  -renewable=false \
  -format=json > vault-secret-guide-token.json
chmod 600 vault-secret-guide-token.json
jq -e '.auth.client_token and (.auth.policies | index("apisix-secret-guide"))' vault-secret-guide-token.json >/dev/null

Do not print or commit either token file. The child token expires after one hour and the cleanup step destroys the entire in-memory Vault server.

What changed: Vault contains one generated credential, and the APISIX token can read only its exact path.

Step 3: Configure the Vault secret provider

Build the request from the protected child-token file and create the fixed provider resource:

jq -n \
  --arg token "$(jq -r '.auth.client_token' vault-secret-guide-token.json)" \
  '{
    uri:"http://apisix-secret-guide-vault:8200",
    prefix:"apisix-kv",
    token:$token
  }' | \
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/secrets/vault/vault-secret-guide" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  --data-binary @- | jq -e '.value.id == "vault-secret-guide"'

The Admin API resource contains the Vault token required by the APISIX provider. Restrict access to the Admin API and etcd, and delete the provider immediately after the test.

What changed: APISIX can authenticate to the isolated Vault with a one-hour, path-scoped token.

Step 4: Create the Consumer credential

Create a fixed Consumer and reference the key-auth field stored at the Vault consumer path:

curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/consumers/vault-secret-guide" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"username":"vault-secret-guide"}' | jq -e '.value.username == "vault-secret-guide"'
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/consumers/vault-secret-guide/credentials/vault-secret-guide-key-auth" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{"plugins":{"key-auth":{"key":"$secret://vault/vault-secret-guide/consumer/key-auth"}}}' | \
  jq -e '.value.id == "vault-secret-guide-key-auth"'

What changed: the Consumer credential refers to Vault; its plaintext authentication value is not stored in the APISIX credential resource.

Step 5: Create a controlled upstream and authenticated Route

Start a pinned local upstream and wait for its listener:

docker run -d --name vault-secret-guide-upstream \
  --network apisix-quickstart-net \
  hashicorp/http-echo:1.0.0 -listen=:5678 -text=vault-secret-guide-upstream
for attempt in {1..15}; do
  docker exec apisix-quickstart bash -c '</dev/tcp/vault-secret-guide-upstream/5678' >/dev/null 2>&1 && break
  sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/vault-secret-guide-upstream/5678'

Create a fixed-ID Route protected by key-auth:

curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/vault-secret-guide-route" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "uri": "/vault-secret-guide",
    "plugins": {"key-auth": {}},
    "upstream": {
      "type": "roundrobin",
      "nodes": {"vault-secret-guide-upstream:5678": 1}
    }
  }' | jq -e '.value.id == "vault-secret-guide-route" and (.value.plugins["key-auth"] | type == "object")'

What changed: the fixed path now authenticates against a key resolved from Vault before proxying to the local upstream.

Validate

Require an exact 200 response and body with the generated credential, then require a wrong key to return 401:

test "$(curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -o vault-secret-guide-body.txt -w '%{http_code}' \
  "${GATEWAY_URL}/vault-secret-guide" -H "apikey: $(cat vault-secret-guide-key.txt)")" = 200
test "$(cat vault-secret-guide-body.txt)" = "vault-secret-guide-upstream"
test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' \
  "${GATEWAY_URL}/vault-secret-guide" -H 'apikey: wrong-key')" = 401

All three assertions must pass. They prove successful Vault resolution, authentication enforcement, and routing to the intended upstream without printing the credential.

Roll back / Clean up

Remove APISIX dependencies first, then delete both disposable containers and every local secret file:

curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/vault-secret-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/vault-secret-guide/credentials/vault-secret-guide-key-auth" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/consumers/vault-secret-guide" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/secrets/vault/vault-secret-guide" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f vault-secret-guide-upstream
docker rm -f apisix-secret-guide-vault
rm -f vault-secret-guide-root-token.txt vault-secret-guide-root.env vault-secret-guide-key.txt \
  vault-secret-guide-token.json vault-secret-guide-body.txt
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL

Expected: the fixed APISIX resources, both containers, the in-memory Vault data, and all local token and secret files are absent.

Troubleshoot

SymptomCauseFix
APISIX logs connection refused for Vault.The Vault container is not ready or is not attached to apisix-quickstart-net.Check docker logs apisix-secret-guide-vault and inspect only the container network membership; do not print its environment.
The valid key returns 401.The child token expired, the provider prefix or secret URI is wrong, or the policy does not cover apisix-kv/consumer.Recreate the one-hour token if necessary and compare the provider, policy path, and secret reference without printing token or key values.
The valid key returns 502.Authentication passed but the local upstream is unavailable.Confirm the pinned upstream is running on apisix-quickstart-net and its port 5678 is reachable from APISIX.
Cleanup leaves Vault data behind.A different persistent Vault server was targeted.Stop and identify the exact server before deleting anything; this guide authorizes removal only of apisix-secret-guide-vault.