Configure HMAC Authentication
Sign requests with HMAC, verify them at API7 Gateway, and confirm the resolved consumer identity.
Applies to:API7 Gateway 3.10.x
Roles:Platform engineer
Goal
A route that rejects unsigned requests and accepts a freshly signed HMAC request for a named consumer.
The client signs the request target and timestamp with a shared secret, and the hmac-auth (opens in Plugin Hub docs) plugin verifies that signature against a consumer credential. clock_skew: 300 limits how long a dated signature is accepted; it limits the replay window rather than storing or rejecting a reused signature. This example also removes the Authorization header before proxying.
Before you begin
Set the variables before running any checks:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default" # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo-
An API7 Gateway control plane is reachable and your Dashboard token is valid:
curl -ks -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" \ -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected:
200. -
A gateway instance in the selected group reports
Healthy:curl -ks "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" \ -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -lExpected:
1or more. -
Required local tools are available:
jq --version python3 --versionExpected:
jqand Python version strings. -
For the ADC path, ADC is installed and supports label selectors:
adc --version adc sync --help | grep -- '--label-selector'Expected: a version string and a help entry for
--label-selector. The option is documented in ADCv0.30.3; use a version whose help lists it. ADC looks up--gateway-groupby gateway group display name, soAPI7_GATEWAY_GROUP_NAMEis distinct from the UUID used by the compatibility Admin API. Enter the name of the same group identified byAPI7_GATEWAY_GROUP_ID. If similar group names make the lookup ambiguous, stop and confirm which group your installed ADC resolves before continuing.ADC writes
diff.yamlin the current directory. Before saving or running this guide's ADC examples, confirm that none of its local paths already exists:for file in hmac-request.py hmac-auth-adc.yaml hmac-auth-cleanup.yaml diff.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; doneExpected: no output. This makes the local cleanup safe; otherwise use a new empty working directory.
This guide creates only these gateway resources: hmac-auth-httpbin-service, hmac-auth-route, hmac-auth-consumer, and hmac-auth-consumer-cred. Check that the first three names are unused:
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/hmac-auth-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/routes/hmac-auth-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/consumers/hmac-auth-consumer?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"For the Admin API path, expected: each response reports that the requested resource is absent. If a response has HTTP status 200 and a populated matching value.id or value.username, that ID is already in use; stop or change every occurrence before continuing. Do not treat another error as proof of absence: correct the token, gateway group, or request until the result is unambiguous. The PUT operations below update a matching ID.
ADC can encounter an object with the same name under a different backend ID, so this check alone does not prove ADC ownership. On the ADC path, continue only if adc diff shows creates for exactly the four example objects and no update, replacement, or deletion of an existing object.
Steps
Choose either the Admin API path in steps 1–3 or the ADC path in step 4. Do not apply both.
Step 1: Create a service with an upstream
curl -ks "${API7_ADMIN_URL}/apisix/admin/services/hmac-auth-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "hmac-auth-httpbin-service",
"labels": {"guide": "hmac-auth"},
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [{"host": "httpbin.org", "port": 80, "weight": 100}]
}
}'{ "value": { "id": "hmac-auth-httpbin-service", "name": "hmac-auth-httpbin-service", … } }What changed: the example service now sends traffic to httpbin.org; no route points to it yet.
Step 2: Create a route and enable hmac-auth
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/hmac-auth-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "hmac-auth-route",
"labels": {"guide": "hmac-auth"},
"paths": ["/anything/hmac-auth"],
"methods": ["GET"],
"service_id": "hmac-auth-httpbin-service",
"plugins": {"hmac-auth": {"clock_skew": 300, "allowed_algorithms": ["hmac-sha256"], "hide_credentials": true}}
}'{ "value": { "id": "hmac-auth-route", "name": "hmac-auth-route", … } }What changed: /anything/hmac-auth now requires a valid hmac-auth credential. With no credential yet, every request is rejected.
Step 3: Create a consumer and credential
curl -ks "${API7_ADMIN_URL}/apisix/admin/consumers/hmac-auth-consumer?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{"username": "hmac-auth-consumer", "labels": {"guide": "hmac-auth"}}'
curl -ks "${API7_ADMIN_URL}/apisix/admin/consumers/hmac-auth-consumer/credentials/hmac-auth-consumer-cred?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "hmac-auth-consumer-cred",
"plugins": {"hmac-auth": {"key_id": "client-1", "secret_key": "my-hmac-secret"}}
}'{ "value": { "username": "hmac-auth-consumer", … } }
{ "value": { "id": "hmac-auth-consumer-cred", "name": "hmac-auth-consumer-cred", … } }What changed: client-1 and its secret resolve to hmac-auth-consumer.
Step 4: Apply the same configuration with ADC
Skip this alternative if you completed steps 1–3. Save the complete desired state:
services:
- name: hmac-auth-httpbin-service
labels:
guide: hmac-auth
upstream:
name: default
scheme: http
nodes:
- host: httpbin.org
port: 80
weight: 100
routes:
- name: hmac-auth-route
labels:
guide: hmac-auth
uris:
- /anything/hmac-auth
methods:
- GET
plugins:
hmac-auth:
clock_skew: 300
allowed_algorithms:
- hmac-sha256
hide_credentials: true
consumers:
- username: hmac-auth-consumer
labels:
guide: hmac-auth
credentials:
- name: hmac-auth-consumer-cred
type: hmac-auth
config:
key_id: client-1
secret_key: my-hmac-secretLint locally, ask the backend to validate the file, and inspect the diff:
adc lint -f hmac-auth-adc.yaml
adc validate -f hmac-auth-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=hmac-auth
adc diff -f hmac-auth-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=hmac-authExpected: lint and validation succeed, and the diff contains only this example's service, nested route, consumer, and credential. The selector scopes reconciliation to top-level resources labeled guide=hmac-auth; the route remains part of its selected service.
After reviewing the diff, apply it:
adc sync -f hmac-auth-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=hmac-authExpected: ADC applies only the changes shown for the guide=hmac-auth partition.
What changed: ADC reconciled the reviewed desired state within the guide=hmac-auth partition.
Validate
An unsigned request should be rejected:
curl -is "${GATEWAY_URL}/anything/hmac-auth" | head -1HTTP/1.1 401 UnauthorizedSave a client that creates a new timestamp and signature each time it runs:
import base64
import hashlib
import hmac
import json
import os
import urllib.request
from email.utils import formatdate
key_id = "client-1"
secret_key = b"my-hmac-secret"
request_path = "/anything/hmac-auth"
date_header = formatdate(timeval=None, localtime=False, usegmt=True)
signing_string = f"{key_id}\nGET {request_path}\ndate: {date_header}\n"
signature = base64.b64encode(
hmac.new(secret_key, signing_string.encode(), hashlib.sha256).digest()
).decode()
request = urllib.request.Request(
os.environ["GATEWAY_URL"] + request_path,
headers={
"Date": date_header,
"Authorization": (
f'Signature keyId="{key_id}",algorithm="hmac-sha256",'
f'headers="@request-target date",signature="{signature}"'
),
},
)
with urllib.request.urlopen(request, timeout=20) as response:
body = json.load(response)
headers = body["headers"]
print(response.status)
print(json.dumps({
"X-Consumer-Username": headers.get("X-Consumer-Username"),
"X-Credential-Identifier": headers.get("X-Credential-Identifier"),
"authorization_forwarded": "Authorization" in headers,
}, indent=2))python3 hmac-request.py200{
"X-Consumer-Username": "hmac-auth-consumer",
"X-Credential-Identifier": "hmac-auth-consumer-cred",
"authorization_forwarded": false
}Roll back / Clean up
Use the cleanup path that matches how you created the resources. Both paths are limited to this guide's IDs or label partition.
Delete the four fixed IDs in reverse dependency order:
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/consumers/hmac-auth-consumer/credentials/hmac-auth-consumer-cred?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/consumers/hmac-auth-consumer?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/hmac-auth-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/hmac-auth-httpbin-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected: each command prints 200.
Save an empty desired state for this guide's label partition:
services: []
consumers: []Lint it and review the deletions before applying them:
adc lint -f hmac-auth-cleanup.yaml
adc diff -f hmac-auth-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=hmac-authExpected: the diff deletes only the service, its nested route, the consumer, and credential created under guide=hmac-auth. Stop if it lists anything else.
adc sync -f hmac-auth-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=hmac-authExpected: ADC removes only the reviewed resources in this label partition.
After cleanup, the example route no longer handles /anything/hmac-auth. Another route can still match that path, so the data-plane response is not guaranteed to be 404.
Remove only the local example files you saved:
rm -f hmac-request.py hmac-auth-adc.yaml hmac-auth-cleanup.yaml diff.yamlExpected: no output.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
The signed request returns 401; after changing signed data it can say {"message":"client request can't be validated"}. | The key ID, secret, request path, signed header list, timestamp, or another signed component differs from what the gateway verifies. | Compare step 3 values, keep the exact GET /anything/hmac-auth signing string, and regenerate the signature after every signed change. |
| The request works briefly and then fails. | Its Date is outside the 300-second clock-skew window. | Run the script again and synchronize the client and gateway clocks. |
| ADC proposes unrelated changes. | The selector is missing or another resource uses the same guide label. | Stop before sync; use the exact selector and reserve guide=hmac-auth for this example. |
Related
hmac-authplugin reference (opens in Plugin Hub docs) — review every signing and verification option.- Consumers and credentials — understand how credentials resolve to consumer identities.
- Configure JWT authentication — use signed bearer tokens instead of signing each request.