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
- Complete Set Up Ingress Controller and Gateway.
Enable SSL on the Gateway
Ensure that your gateway has enabled SSL.
- APISIX Gateway
- API7 Gateway
helm upgrade apisix apisix/apisix \
--set ... \ # add other parameters
--set "apisix.ssl.enabled=true" \
--set "apisix.ssl.containerPort=9443"
By default, API7 Gateway enables SSL on container port 9443 and service port 443, so no additional configuration is required for this step.
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
- Gateway API
- APISIX CRD
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:
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 two Kubernetes secrets:
kubectl create secret tls test-mtls-secret \
--cert=server.crt \
--key=server.key \
--namespace=aic
kubectl create secret generic test-ca-secret \
--from-file=cert=ca.crt \
--namespace=aic
Create a Kubernetes manifest file for mTLS configuration:
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
namespace: aic
name: test-mtls
spec:
ingressClassName: apisix
hosts:
- test.com
secret:
name: test-mtls-secret
namespace: aic
client:
caSecret:
name: test-ca-secret
namespace: aic
depth: 1
Apply the configuration to your cluster:
kubectl apply -f mtls.yaml
Create a Route
- Gateway API
- APISIX CRD
Create an ExternalName Service and HTTPRoute that forward /ip requests to httpbin.org:
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
Apply the configuration:
kubectl apply -f httpbin-route.yaml
Create an example route that forwards all requests with the path /ip to the upstream service httpbin.org:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: httpbin-ip
spec:
ingressClassName: apisix
http:
- name: httpbin-ip
match:
paths:
- /ip
upstreams:
- name: httpbin-external-domain
Apply the configuration to your cluster:
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.