Docs
Ingress ControllerManage traffic and APIsTraffic ManagementTLS and mTLSConfigure mTLS Between Client and Gateway

Configure mTLS Between Client and Gateway

Learn how to use APISIX or API7 Ingress Controller to configure the gateway to require mutual TLS (mTLS) from clients.

Applies to:API7 Ingress Controller (drives API7 Gateway)APISIX Ingress Controller (drives Apache APISIX)

Goal

Configure the gateway to require clients to present a trusted certificate for mutual TLS (mTLS) authentication.

mTLS

Client

APISIX / API7 Gateway

Upstream Service

mTLS

Client

APISIX / API7 Gateway

Upstream Service

Before you begin

  • Complete Set Up Ingress Controller and Gateway.
  • You have kubectl configured against the cluster used in the setup guide.
  • You have OpenSSL installed to create the test CA, server, and client certificates.
  • Choose a free local port for the gateway HTTPS port-forward. The examples use 9443.

Set the variables this guide uses:

export TLS_SNI="test.com"
export GATEWAY_SERVICE_NAME="apisix-gateway"   # your gateway's Service name in namespace aic
export GATEWAY_HTTPS_PORT="9443"

Steps

Step 1: Enable SSL on the gateway

Ensure that your gateway has enabled SSL.

helm upgrade apisix apisix/apisix \
  --set "apisix.ssl.enabled=true" \
  --set "apisix.ssl.containerPort=9443"

Step 2: Generate the test CA and certificates

Generate the Certificate Authority (CA) key and certificate:

openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 36500 -sha256 \
  -key ca.key \
  -out ca.crt \
  -subj "/CN=MyTestCA" \
  -extensions v3_ca \
  -config <(printf "[req]\ndistinguished_name=req\n[ v3_ca ]\nbasicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer")

Generate the server key and certificate signing request (CSR), then sign it with the CA:

openssl genrsa -out server.key 2048
openssl req -new -sha256 \
  -key server.key \
  -out server.csr \
  -subj "/CN=${TLS_SNI}"

openssl x509 -req -days 36500 -sha256 \
  -in server.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt \
  -extensions v3_req \
  -extfile <(printf "[v3_req]\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=DNS:%s" "${TLS_SNI}")

Generate the client key and CSR, then sign it with the CA:

openssl genrsa -out client.key 2048
openssl req -new -sha256 \
  -key client.key \
  -out client.csr \
  -subj "/CN=CLIENT"

openssl x509 -req -days 36500 -sha256 \
  -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt \
  -extensions v3_req \
  -extfile <(printf "[v3_req]\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=clientAuth")

Step 3: Configure mTLS for the gateway

Create the server certificate Secret and a ConfigMap containing the client CA certificate. Gateway API reads CA certificates from the ca.crt key:

kubectl create secret tls test-mtls-secret \
  --cert=server.crt \
  --key=server.key \
  --namespace=aic

kubectl create configmap test-client-ca \
  --from-file=ca.crt=ca.crt \
  --namespace=aic

Create a Gateway that requires clients to present a certificate signed by the CA. spec.tls.frontend.default applies the validation configuration to all HTTPS listeners unless a listener port has an entry under spec.tls.frontend.perPort:

mtls-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: apisix
  namespace: aic
spec:
  gatewayClassName: apisix
  tls:
    frontend:
      default:
        validation:
          caCertificateRefs:
          - group: ""
            kind: ConfigMap
            name: test-client-ca
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: test.com
    tls:
      mode: Terminate
      certificateRefs:
      - group: ""
        kind: Secret
        name: test-mtls-secret
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-config

Apply the configuration:

kubectl apply -f mtls-gateway.yaml

The Ingress Controller also accepts a Secret in caCertificateRefs as an implementation-specific extension. A cross-namespace CA reference requires a ReferenceGrant. AllowInsecureFallback is not supported because the gateway cannot accept clients that omit a certificate or fail certificate validation.

Step 4: Create a route

Create an ExternalName Service and HTTPRoute that forward /ip requests to httpbin.org:

httpbin-route.yaml
apiVersion: v1
kind: Service
metadata:
  name: httpbin-external-domain
  namespace: aic
spec:
  type: ExternalName
  externalName: httpbin.org
  ports:
  - name: http
    port: 80
    appProtocol: http
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-ip
  namespace: aic
spec:
  parentRefs:
  - name: apisix
    sectionName: https
  hostnames:
  - test.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /ip
    backendRefs:
    - name: httpbin-external-domain
      port: 80

This example follows the Getting Started configuration with listener-port matching disabled, so the route can match when Service port 443 maps to container port 9443. If you enable matching, review the effective mode and physical gateway ports in the configuration reference.

Apply the configuration:

kubectl apply -f httpbin-route.yaml

Validate

Forward the gateway's SSL port to your local machine:

kubectl port-forward -n aic "svc/${GATEWAY_SERVICE_NAME}" "${GATEWAY_HTTPS_PORT}:443" &

With client certificate

Since the certificate is valid for ${TLS_SNI}, use ${TLS_SNI} as the gateway's domain name. Send a request to the route with the client certificate:

curl -iv --cacert ca.crt \
  --resolve "${TLS_SNI}:${GATEWAY_HTTPS_PORT}:127.0.0.1" "https://${TLS_SNI}:${GATEWAY_HTTPS_PORT}/ip" \
  --cert client.crt --key client.key

Expected: an mTLS handshake completes and the response contains HTTP/2 200.

Without client certificate

Send a request to the route without a client certificate:

curl -iv --cacert ca.crt \
  --resolve "${TLS_SNI}:${GATEWAY_HTTPS_PORT}:127.0.0.1" "https://${TLS_SNI}:${GATEWAY_HTTPS_PORT}/ip"

Expected: the handshake fails because the gateway rejects the client that does not present a trusted certificate.

Roll back / Clean up

Delete the manifests, secrets, and ConfigMap created in this guide:

kubectl delete -f httpbin-route.yaml
kubectl delete -f mtls-gateway.yaml
kubectl delete -f mtls.yaml
kubectl delete secret test-mtls-secret -n aic
kubectl delete configmap test-client-ca -n aic
kubectl delete secret test-ca-secret -n aic
rm -f ca.key ca.crt ca.srl server.key server.csr server.crt client.key client.csr client.crt

Stop the local port-forward process if it is still running:

kill %1 2>/dev/null || true

Troubleshoot

SymptomCauseFix
curl reports a certificate error when using the client certificate.The server certificate CN does not match the requested SNI, or the client certificate is not signed by the CA trusted by the gateway.Confirm TLS_SNI is the value used in the OpenSSL -subj and the Gateway/ApisixTls hosts/hostname, and that the client CSR was signed by the same CA configured in test-client-ca or test-ca-secret.
curl succeeds without a client certificate.Client certificate validation is not enabled, or the Gateway/ApisixTls resource is not reconciled.Verify the Gateway has tls.frontend.default.validation.caCertificateRefs (Gateway API) or client.caSecret (APISIX CRD), and that the route parentRefs resolve.
The route returns 404.The HTTPRoute or ApisixRoute is not reconciled, or it does not match the requested path.Check that the route parentRefs resolve and that the path is /ip.