Skip to main content

Configure mTLS Between Client and Gateway

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.

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

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

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.