Configure mTLS Between Client and APISIX
Require and verify a client certificate at Apache APISIX, prove the rejection path, and remove every test resource.
Applies to:Apache APISIX 3.18.0
Deployments:DockerRoles:API developerPlatform engineerTime:25 minutesImpact:Changes live traffic
Goal
Configure mutual TLS (mTLS) for mtls-guide.local so APISIX and the client authenticate each other. Prove both outcomes: a trusted client certificate reaches a controlled upstream, while a request without a client certificate fails during the TLS exchange.
This procedure uses a private, short-lived test CA in an isolated APISIX quickstart. Production deployments should separate server and client trust according to organizational policy and protect all private keys with the approved secret-management workflow.
Before you begin
- Complete Get APISIX with the Docker quickstart.
- Install cURL, jq, and OpenSSL.
- Confirm port
9443from the quickstart is reachable only from the intended test machine.
Set the endpoints and Admin API key:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_MTLS_URL="https://mtls-guide.local:9443"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echoRequire the Admin API and protect every fixed resource and local artifact:
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-client-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route mtls-client-guide-route already exists'; exit 1; }
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/ssls/mtls-client-guide-ssl" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: SSL mtls-client-guide-ssl already exists'; exit 1; }
test -z "$(docker ps -aq --filter name=^/mtls-client-guide-upstream$)" || { echo 'Stop: container mtls-client-guide-upstream already exists'; exit 1; }
for file in mtls-guide-ca.key mtls-guide-ca.crt mtls-guide-server.key mtls-guide-server.csr mtls-guide-server.crt mtls-guide-server.ext mtls-guide-client.key mtls-guide-client.csr mtls-guide-client.crt mtls-guide-client.ext mtls-guide-ca.srl mtls-guide-body.txt; do
test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }
doneSteps
Step 1: Start a controlled upstream and create a Route
Run a pinned local echo server on the quickstart network and wait for its listener:
docker run -d --name mtls-client-guide-upstream \
--network apisix-quickstart-net \
hashicorp/http-echo:1.0.0 -listen=:5678 -text=mtls-client-guide-upstream
for attempt in {1..15}; do
docker exec apisix-quickstart bash -c '</dev/tcp/mtls-client-guide-upstream/5678' >/dev/null 2>&1 && break
sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/mtls-client-guide-upstream/5678'Create a fixed-ID Route with Admin API authentication:
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-client-guide-route" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
-d '{
"uri": "/mtls-client-guide",
"upstream": {
"type": "roundrobin",
"nodes": {"mtls-client-guide-upstream:5678": 1}
}
}' | jq -e '.value.id == "mtls-client-guide-route"'Step 2: Issue short-lived server and client certificates
Create a private test CA and a seven-day server certificate with a Subject Alternative Name:
umask 077
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 7 \
-keyout mtls-guide-ca.key -out mtls-guide-ca.crt -subj '/CN=APISIX mTLS Guide CA'
openssl req -new -newkey rsa:2048 -nodes -sha256 \
-keyout mtls-guide-server.key -out mtls-guide-server.csr -subj '/CN=mtls-guide.local'
cat > mtls-guide-server.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:mtls-guide.local
EOF
openssl x509 -req -sha256 -days 7 \
-in mtls-guide-server.csr \
-CA mtls-guide-ca.crt -CAkey mtls-guide-ca.key -CAcreateserial \
-extfile mtls-guide-server.ext -out mtls-guide-server.crtIssue a seven-day client certificate with the client-authentication purpose:
openssl req -new -newkey rsa:2048 -nodes -sha256 \
-keyout mtls-guide-client.key -out mtls-guide-client.csr -subj '/CN=mtls-guide-client'
cat > mtls-guide-client.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth
EOF
openssl x509 -req -sha256 -days 7 \
-in mtls-guide-client.csr \
-CA mtls-guide-ca.crt -CAkey mtls-guide-ca.key -CAserial mtls-guide-ca.srl \
-extfile mtls-guide-client.ext -out mtls-guide-client.crt
openssl verify -purpose sslserver -CAfile mtls-guide-ca.crt mtls-guide-server.crt
openssl verify -purpose sslclient -CAfile mtls-guide-ca.crt mtls-guide-client.crtExpected: OpenSSL reports OK for both certificates.
Step 3: Create the mTLS SSL object
Build the JSON with jq so PEM newlines are escaped correctly. The client.ca value makes APISIX require a client certificate issued by the test CA:
jq -n \
--arg sni 'mtls-guide.local' \
--arg cert "$(cat mtls-guide-server.crt)" \
--arg key "$(cat mtls-guide-server.key)" \
--arg client_ca "$(cat mtls-guide-ca.crt)" \
'{sni: $sni, cert: $cert, key: $key, client: {ca: $client_ca}}' | \
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/ssls/mtls-client-guide-ssl" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
--data-binary @- | jq -e '.value.id == "mtls-client-guide-ssl" and .value.sni == "mtls-guide.local" and (.value.client.ca | length > 0)'Do not print the request body or commit the generated keys and certificates.
Validate
Send a request with the trusted client certificate. Require both server-certificate verification and the exact upstream body:
curl --fail-with-body -sS --connect-timeout 3 --max-time 15 \
--cacert mtls-guide-ca.crt \
--cert mtls-guide-client.crt --key mtls-guide-client.key \
--resolve 'mtls-guide.local:9443:127.0.0.1' \
-o mtls-guide-body.txt \
"${GATEWAY_MTLS_URL}/mtls-client-guide"
test "$(cat mtls-guide-body.txt)" = "mtls-client-guide-upstream"Now omit the client certificate and require the TLS request to fail:
if curl --fail-with-body -sS --connect-timeout 3 --max-time 15 \
--cacert mtls-guide-ca.crt \
--resolve 'mtls-guide.local:9443:127.0.0.1' \
"${GATEWAY_MTLS_URL}/mtls-client-guide"; then
echo 'Unexpected success without a client certificate'
exit 1
fiBoth the positive and negative assertions are required. Do not add --insecure or -k; that would bypass the server-certificate and hostname checks.
Roll back / Clean up
Delete only the fixed resources created by this guide, remove the upstream, and remove all local private-key material:
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-client-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/ssls/mtls-client-guide-ssl" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f mtls-client-guide-upstream
rm -f mtls-guide-ca.key mtls-guide-ca.crt mtls-guide-server.key mtls-guide-server.csr \
mtls-guide-server.crt mtls-guide-server.ext mtls-guide-client.key mtls-guide-client.csr \
mtls-guide-client.crt mtls-guide-client.ext mtls-guide-ca.srl mtls-guide-body.txt
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_MTLS_URLExpected: both Admin API resources and the container are absent, and no generated key or certificate remains in the working directory.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| The positive request reports an unknown CA or hostname mismatch. | The wrong CA is selected, or APISIX serves another SNI certificate. | Keep the URL and --resolve hostname aligned, then confirm the fixed SSL object's SNI and certificate. |
| The positive request reports that a client certificate is required. | The client certificate or key option is missing, or the certificate is not issued by mtls-guide-ca.crt. | Verify the client certificate purpose and issuer, then pass both --cert and --key. |
| The request without a client certificate succeeds. | Another SSL object matched, or client verification is not present on the matched object. | Read back mtls-client-guide-ssl, confirm client.ca, and verify that the request SNI is mtls-guide.local. |
| The TLS request succeeds but the body differs. | Another Route matched or the Route points to another upstream. | Read back mtls-client-guide-route and verify its exact URI and node. |
| Cleanup of the SSL object fails. | The Admin API key is wrong or the object ID differs. | Correct authentication and delete only mtls-client-guide-ssl; do not delete all SSL objects. |