API7 Docs
How-To GuidesTraffic ManagementTLS and mTLS

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.

Mutual TLS (mTLS) requires the client and gateway to authenticate each other. This guide configures the gateway to trust a client CA, present its server certificate, and reject clients that do not provide a trusted certificate.

mTLS

Client

APISIX / API7 Gateway

Upstream Service

mTLS

Client

APISIX / API7 Gateway

Upstream Service

Prerequisite

  1. Complete Set Up Ingress Controller and Gateway.

Enable SSL on the Gateway

Ensure that your gateway has enabled SSL.

helm upgrade apisix apisix/apisix \
  --set ... \ # add other parameters
  --set "apisix.ssl.enabled=true" \
  --set "apisix.ssl.containerPort=9443"

Generate Certificates and Keys

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 key and certificate signing request (CSR):

openssl genrsa -out server.key 2048
openssl req -new -sha256 \
  -key server.key \
  -out server.csr \
  -subj "/CN=test.com"

Sign the server CSR with the CA certificate to generate the server certificate:

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:test.com")

Generate the key and certificate signing request (CSR) for the client:

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

Sign the client CSR with the CA certificate to generate the client certificate:

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")

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.

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

The Gateway listener uses port 443, while APISIX and API7 Gateway typically terminate TLS on container port 9443. API7 Ingress Controller injects a server_port match for 443 when sectionName: https explicitly selects this listener and listener_port_match_mode is auto or explicit, so the route does not match traffic received on 9443. The API7 chart defaults the mode to off.

APISIX Ingress Controller does not inject a server_port match for this example because the listener has a hostname. The Getting Started APISIX install still sets the mode to off to avoid mismatches for routes that explicitly target a hostname-less listener or match multiple hostname-less listener ports.

Apply the configuration:

kubectl apply -f httpbin-route.yaml

Verify

To verify mTLS between the client and the gateway, first port-forward the gateway’s SSL port to your local machine:

kubectl port-forward svc/<your-gateway-svc-name> 9443:443 &

With Client Certificate

Since the certificate is valid for test.com, use test.com as the gateway’s domain name. Send a request to the route with the client certificate:

curl -iv --cacert ca.crt \
  --resolve "test.com:9443:127.0.0.1" "https://test.com:9443/ip" \
  --cert client.crt --key client.key

An mTLS handshake similar to the following verifies the mTLS between the client and the gateway is enabled:

* Added test.com:9443:127.0.0.1 to DNS cache
* Hostname test.com was found in DNS cache
*   Trying 127.0.0.1:9443...
* Connected to test.com (127.0.0.1) port 9443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: ca.crt
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
...
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200 
HTTP/2 200 
...

Note that the gateway and the client successfully verified each other's certificate during the handshake and established a connection.

Without Client Certificate

Send a request to https://test.com:9443/ip but without client certificate:

curl -iv --cacert ca.crt \
  --resolve "test.com:9443:127.0.0.1" "https://test.com:9443/ip"

A failed mTLS handshake is similar to the following:

* Added test.com:9443:127.0.0.1 to DNS cache
* Hostname test.com was found in DNS cache
*   Trying 127.0.0.1:9443...
* Connected to test.com (127.0.0.1) port 9443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: ca.crt
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
...
* TLSv1.3 (IN), TLS alert, unknown (628):
* OpenSSL SSL_read: error:1409445C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required, errno 0
* Failed receiving HTTP2 data
* OpenSSL SSL_write: SSL_ERROR_ZERO_RETURN, errno 0
* Failed sending HTTP2 data
* Connection #0 to host test.com left intact

The handshake failed due to the lack of client certificate.