Docs
Apache APISIXManage traffic and APIsTraffic ManagementTLS and mTLSConfigure mTLS Between Client and APISIX

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 9443 from 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 && echo

Require 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; }
done

Steps

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

Issue 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.crt

Expected: 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
fi

Both 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_URL

Expected: both Admin API resources and the container are absent, and no generated key or certificate remains in the working directory.

Troubleshoot

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