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

Configure mTLS Between APISIX and Upstream

Configure Apache APISIX to authenticate to a controlled HTTPS upstream with a client certificate, verify both outcomes, and clean up safely.

Applies to:Apache APISIX 3.18.0

Deployments:DockerRoles:API developerPlatform engineerTime:25 minutesImpact:Changes live traffic

Goal

Configure APISIX to present a client certificate to a pinned local NGINX upstream. Prove that the upstream rejects a client without a certificate and that the APISIX Route reaches the exact response after mTLS client authentication is configured.

This guide proves the upstream's authentication of APISIX. The generic HTTPS Upstream configuration shown here does not assert that APISIX validates the upstream server certificate; configure and verify deployment-wide upstream trust separately when bidirectional identity verification is required.

Before you begin

  • Complete Get APISIX with the Docker quickstart.
  • Install cURL, jq, and OpenSSL.

Set the endpoints and Admin API key:

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

Protect the fixed Route, container, and local directory:

test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-upstream-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route mtls-upstream-guide-route already exists'; exit 1; }
test -z "$(docker ps -aq --filter name=^/mtls-upstream-guide$)" || { echo 'Stop: container mtls-upstream-guide already exists'; exit 1; }
test ! -e mtls-upstream-guide || { echo 'Stop: directory mtls-upstream-guide already exists'; exit 1; }

Steps

Step 1: Issue short-lived server and client certificates

Create a private test CA and a seven-day server certificate for the container DNS name:

umask 077
mkdir mtls-upstream-guide
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 7 \
  -keyout mtls-upstream-guide/ca.key -out mtls-upstream-guide/ca.crt \
  -subj '/CN=APISIX Upstream mTLS Guide CA'
openssl req -new -newkey rsa:2048 -nodes -sha256 \
  -keyout mtls-upstream-guide/server.key -out mtls-upstream-guide/server.csr \
  -subj '/CN=mtls-upstream-guide'
cat > mtls-upstream-guide/server.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:mtls-upstream-guide
EOF
openssl x509 -req -sha256 -days 7 \
  -in mtls-upstream-guide/server.csr \
  -CA mtls-upstream-guide/ca.crt -CAkey mtls-upstream-guide/ca.key -CAcreateserial \
  -extfile mtls-upstream-guide/server.ext -out mtls-upstream-guide/server.crt

Issue a seven-day client certificate for APISIX:

openssl req -new -newkey rsa:2048 -nodes -sha256 \
  -keyout mtls-upstream-guide/client.key -out mtls-upstream-guide/client.csr \
  -subj '/CN=apisix-mtls-guide-client'
cat > mtls-upstream-guide/client.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth
EOF
openssl x509 -req -sha256 -days 7 \
  -in mtls-upstream-guide/client.csr \
  -CA mtls-upstream-guide/ca.crt -CAkey mtls-upstream-guide/ca.key \
  -CAserial mtls-upstream-guide/ca.srl \
  -extfile mtls-upstream-guide/client.ext -out mtls-upstream-guide/client.crt
openssl verify -purpose sslserver -CAfile mtls-upstream-guide/ca.crt mtls-upstream-guide/server.crt
openssl verify -purpose sslclient -CAfile mtls-upstream-guide/ca.crt mtls-upstream-guide/client.crt

Expected: OpenSSL reports OK for both certificates.

Step 2: Start an upstream that requires a client certificate

Create a minimal NGINX configuration:

cat > mtls-upstream-guide/nginx.conf <<'EOF'
events {}
http {
    server {
        listen 8443 ssl;
        server_name mtls-upstream-guide;
        ssl_certificate /etc/nginx/tls/server.crt;
        ssl_certificate_key /etc/nginx/tls/server.key;
        ssl_client_certificate /etc/nginx/tls/ca.crt;
        ssl_verify_client on;

        location = /mtls-upstream-guide {
            default_type text/plain;
            return 200 "mtls-upstream-guide\n";
        }
    }
}
EOF
docker run -d --name mtls-upstream-guide \
  --network apisix-quickstart-net \
  -v "${GUIDE_DIR}/mtls-upstream-guide/nginx.conf:/etc/nginx/nginx.conf:ro" \
  -v "${GUIDE_DIR}/mtls-upstream-guide:/etc/nginx/tls:ro" \
  nginx:1.29-alpine
for attempt in {1..15}; do
  docker exec mtls-upstream-guide nginx -t >/dev/null 2>&1 && break
  sleep 1
done
docker exec mtls-upstream-guide nginx -t

Require a client without a certificate to fail, then verify the exact response with the issued client certificate:

if docker run --rm --network apisix-quickstart-net \
  -v "${GUIDE_DIR}/mtls-upstream-guide/ca.crt:/tmp/ca.crt:ro" \
  curlimages/curl:8.17.0 --fail-with-body -sS \
  --cacert /tmp/ca.crt https://mtls-upstream-guide:8443/mtls-upstream-guide; then
  echo 'Unexpected success without a client certificate'
  exit 1
fi
docker run --rm --network apisix-quickstart-net \
  -v "${GUIDE_DIR}/mtls-upstream-guide:/tmp/tls:ro" \
  curlimages/curl:8.17.0 --fail-with-body -sS \
  --cacert /tmp/tls/ca.crt --cert /tmp/tls/client.crt --key /tmp/tls/client.key \
  https://mtls-upstream-guide:8443/mtls-upstream-guide | grep -Fqx 'mtls-upstream-guide'

Step 3: Create the APISIX Route

Build the Route body with jq so PEM newlines are escaped correctly. The client key is sent only to the local Admin API in this disposable setup:

jq -n \
  --arg cert "$(cat mtls-upstream-guide/client.crt)" \
  --arg key "$(cat mtls-upstream-guide/client.key)" \
  '{
    uri: "/mtls-upstream-guide",
    upstream: {
      type: "roundrobin",
      scheme: "https",
      nodes: {"mtls-upstream-guide:8443": 1},
      tls: {client_cert: $cert, client_key: $key}
    }
  }' | \
curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-upstream-guide-route" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  --data-binary @- | jq -e '.value.id == "mtls-upstream-guide-route" and .value.upstream.scheme == "https" and (.value.upstream.tls.client_cert | length > 0)'

Validate

Require the exact upstream response through APISIX:

test "$(curl --fail-with-body -sS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/mtls-upstream-guide")" = "mtls-upstream-guide"

The earlier negative test proves that NGINX rejects clients without a certificate. The exact response through APISIX proves that APISIX presented the configured client certificate and completed the HTTPS request.

Roll back / Clean up

Delete the fixed Route before removing the certificate files it references, then remove the container and local directory:

curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/mtls-upstream-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f mtls-upstream-guide
rm -f mtls-upstream-guide/ca.key mtls-upstream-guide/ca.crt mtls-upstream-guide/ca.srl \
  mtls-upstream-guide/server.key mtls-upstream-guide/server.csr mtls-upstream-guide/server.crt \
  mtls-upstream-guide/server.ext mtls-upstream-guide/client.key mtls-upstream-guide/client.csr \
  mtls-upstream-guide/client.crt mtls-upstream-guide/client.ext mtls-upstream-guide/nginx.conf
rmdir mtls-upstream-guide
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL GUIDE_DIR

Troubleshoot

SymptomCauseFix
The direct request with a certificate fails.The certificate lacks clientAuth, the key does not match, or NGINX trusts another CA.Re-run both OpenSSL purpose checks and inspect the three NGINX TLS paths.
The direct request without a certificate succeeds.NGINX did not load ssl_verify_client on or another server block handled the request.Require nginx -t, inspect the active configuration, and use the exact container hostname.
APISIX returns 502.The Route lacks the client pair, the node is unreachable, or the PEM value was malformed.Read back the fixed Route, confirm both client fields are non-empty, and inspect APISIX and NGINX logs.
APISIX returns 404.Another Route matched or the path differs.Request /mtls-upstream-guide and confirm the fixed Route URI.
Cleanup fails because the directory is not empty.An unexpected artifact was created under the protected directory.Inspect it and remove only files created by this guide; do not use a recursive broad deletion.