For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
mTLS (FrontendTLS)
Create a FrontendTLSConfig on a Gateway to enforce mutual TLS between clients and listeners.
Add a FrontendTLSConfig to a Gateway to create a mutual TLS (mTLS) listener.
About FrontendTLS
When configuring an mTLS listener on a Gateway, the client application and the Gateway must exchange certificates to verify their identities before a connection can be established. After a TLS connection is established, the TLS connection is terminated at the Gateway and the unencrypted HTTP traffic is forwarded to the backend destination.
FrontendTLS supports the following configurations:
- Default (required): Create the default client certificate validation configuration for all Gateway listeners that handle HTTPS traffic. For an example, see the Default configuration for all listeners guide.
- perPort (optional): Override the default configuration with port-specific configuration. The configuration is applied only to matching ports that handle HTTPS traffic. For all other ports that handle HTTPS traffic, the default configuration continues to apply. For an example, see the Per port configuration guide.
In addition, you can choose between the following validation modes:
- AllowValidOnly: A connection between a client and the gateway proxy can only be established if the gateway can validate the client’s TLS certificate successfully. For an example, see the Default configuration for all listeners guide.
- AllowInsecureFallback: The gateway proxy can establish a TLS connection, even if the client TLS certificate could not be validated successfully. For an example, see the Per port configuration guide.
Both the default and perPort configurations are applied at the port level. To isolate the CA certificates of listeners that share a port, override the Gateway configuration for an individual listener by using a ListenerPolicy. For an example, see the Per listener configuration guide.
About this guide
In this guide, you learn how to apply default certificate validation configuration for all HTTPS listeners on a Gateway, how to override this configuration for a specific port, and how to override it for an individual listener so that listeners that share a port can use different CA certificates. You further explore secure and insecure certificate validation modes, and use TLS annotations to limit connections to clients that present certificates with a specific Subject Alt Name and certificate hash.
Throughout this guide, you use self-signed TLS certificates for the Certificate Authority. These certificates are used to sign the TLS certificates for the gateway proxy (server) and httpbin client.
Warning
Self-signed certificates are used for demonstration purposes. Do not use self-signed certificates in production environments. Instead, use certificates that are issued from a trusted Certificate Authority.
Before you begin
Set up kgateway by following the Quick start or Installation guides.
Make sure that you have the OpenSSL version of
openssl, not LibreSSL. Theopensslversion must be at least 1.1.Check the
opensslversion that is installed. If you see LibreSSL in the output, continue to the next step.openssl versionInstall the OpenSSL version (not LibreSSL). For example, you might use Homebrew.
brew install opensslReview the output of the OpenSSL installation for the path of the binary file. You can choose to export the binary to your path, or call the entire path whenever the following steps use an openssl command.
- For example, openssl might be installed along the following path:
/usr/local/opt/openssl@3/bin/ - To run commands, you can append the path so that your terminal uses this installed version of OpenSSL, and not the default LibreSSL.
/usr/local/opt/openssl@3/bin/openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650...
- For example, openssl might be installed along the following path:
- Decide whether to set up a listener inline on the Gateway resource or as a separate ListenerSet resource. For more information, see the Listener overview.
Create TLS certificates
Create self-signed TLS certificates that you use for the mutual TLS connection between your client application (curl) and the gateway proxy.
Warning
Self-signed certificates are used for demonstration purposes. Do not use self-signed certificates in production environments. Instead, use certificates that are issued from a trusted Certificate Authority.
Create the
example_certsdirectory and navigate into this directory.mkdir example_certs && cd example_certsCreate self-signed certificates for the Certificate Authority (CA) that you later use to sign the server and client certificates.
# Create CA private key openssl genrsa -out ca-key.pem 2048 # Create CA certificate (valid for 1 year) openssl req -new -x509 -days 365 -key ca-key.pem -out ca-cert.pem \ -subj "/CN=Test CA/O=Test Org"Create the server certificates for the Gateway that is signed by the CA that you created in the previous step. The Gateway uses these certificates to terminate incoming TLS connections.
# Create server private key openssl genrsa -out server-key.pem 2048 # Create server certificate signing request openssl req -new -key server-key.pem -out server.csr \ -subj "/CN=example.com/O=Test Org" # Create server certificate signed by CA (valid for 1 year) openssl x509 -req -days 365 -in server.csr -CA ca-cert.pem -CAkey ca-key.pem \ -CAcreateserial -out server-cert.pem \ -extensions v3_req -extfile <(echo "[v3_req]"; echo "subjectAltName=DNS:example.com,DNS:*.example.com")Store the server certificate and key in a Kubernetes secret.
# Base64 encode server certificate and key SERVER_CERT=$(cat server-cert.pem | base64 -w 0) SERVER_KEY=$(cat server-key.pem | base64 -w 0) # Create the secret kubectl create secret tls https-cert \ --cert=server-cert.pem \ --key=server-key.pem \ -n kgateway-systemStore the CA certificates in a configmap. The gateway proxy later uses these certificates to validate the client certificate that is presented during the TLS handshake.
kubectl create configmap ca-cert \ --from-file=ca.crt=ca-cert.pem \ -n kgateway-systemCreate a client certificate and private key. You use these credentials later when sending a request to the gateway proxy. The client certificate is signed by the same CA that you used for the gateway proxy.
# Create client private key openssl genrsa -out client-key.pem 2048 # Create client certificate signing request openssl req -new -key client-key.pem -out client.csr \ -subj "/CN=client.example.com/O=Test Org" # Create client certificate signed by CA (valid for 1 year) openssl x509 -req -days 365 -in client.csr -CA ca-cert.pem -CAkey ca-key.pem \ -CAcreateserial -out client-cert.pem \ -extensions v3_req -extfile <(echo "[v3_req]"; echo "subjectAltName=DNS:example.com,DNS:*.example.com")Continue with configuring a Default configuration for all listeners. Alternatively, you can explore how to override the default configuration for a specific port or for an individual listener.
Default configuration for all listeners
Create a Gateway with a default frontend TLS configuration that applies to all listeners that handle HTTPS traffic. The following example configures two HTTPS listeners on the Gateway. Both listeners use the same server TLS credentials to terminate incoming HTTPS connections. The validation mode is set to
AllowValidOnlyto allow connection only if a valid certificate is presented during the TLS handshake.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: mtls namespace: kgateway-system spec: gatewayClassName: kgateway tls: frontend: default: validation: mode: AllowValidOnly caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" listeners: - name: https-8443 protocol: HTTPS port: 8443 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All - name: https-8444 protocol: HTTPS port: 8444 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All EOFCreate an HTTPRoute that routes incoming traffic on the
example.comdomain to the mTLS Gateway that you created.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: httpbin-https namespace: httpbin labels: example: httpbin-route spec: hostnames: - "example.com" parentRefs: - name: mtls namespace: kgateway-system rules: - backendRefs: - name: httpbin port: 8000 EOFGet the external address of the gateway and save it in an environment variable. Note that it might take a few seconds for the gateway address to become available.
export INGRESS_GW_ADDRESS=$(kubectl get svc -n kgateway-system mtls -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo $INGRESS_GW_ADDRESSSend a request to the httpbin app without a client certificate on both 8443 and 8444 ports. Verify that the TLS handshake fails, because a TLS certificate is required to establish the connection.
curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get curl -v -k --resolve "example.com:8444:${INGRESS_GW_ADDRESS}" https://example.com:8444/getExample output:
* LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0 * Failed receiving HTTP2 data: 56(Failure when receiving data from the peer) * Connection #0 to host localhost left intact curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0Repeat the request. This time, you include the client certificate that you created earlier.
curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pem curl -v -k --resolve "example.com:8444:${INGRESS_GW_ADDRESS}" https://example.com:8444/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pemExample output for port 8444:
* Connection #0 to host localhost left intact * Added example.com:8444:127.0.0.1 to DNS cache * Host localhost:8444 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:8444... * Connected to localhost (::1) port 8444 * ALPN: curl offers h2,http/1.1 * (304) (OUT), TLS handshake, Client hello (1): * (304) (IN), TLS handshake, Server hello (2): * (304) (IN), TLS handshake, Unknown (8): * (304) (IN), TLS handshake, Request CERT (13): * (304) (IN), TLS handshake, Certificate (11): * (304) (IN), TLS handshake, CERT verify (15): * (304) (IN), TLS handshake, Finished (20): * (304) (OUT), TLS handshake, Certificate (11): * (304) (OUT), TLS handshake, CERT verify (15): * (304) (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF * ALPN: server accepted h2 * Server certificate: * subject: CN=example.com; O=Test Org * start date: Jan 13 20:19:17 2026 GMT * expire date: Jan 13 20:19:17 2027 GMT * issuer: CN=Test CA; O=Test Org * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://localhost:8444/get * [HTTP/2] [1] [:method: GET] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: example.com] * [HTTP/2] [1] [:path: /get] * [HTTP/2] [1] [user-agent: curl/8.7.1] * [HTTP/2] [1] [accept: */*] > GET /get HTTP/2 > Host: example.com > User-Agent: curl/8.7.1 > Accept: */* * Request completely sent off < HTTP/2 200 < access-control-allow-credentials: true < access-control-allow-origin: * < content-type: application/json; encoding=utf-8 < date: Tue, 13 Jan 2026 21:59:07 GMT < content-length: 515 < x-envoy-upstream-service-time: 1 < server: envoy ...
Per port configuration
In this example, you override the default certificate validation configuration for port 8444.
Update your Gateway to add in port-specific validation configuration for port 8444. In the following example, you override the default certificate validation for port 8444. This configuration allows requests, even if an invalid certificate was presented during the TLS handshake. Port 8443 continues to only allow connections if a valid certificate is presented.
kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: mtls namespace: kgateway-system spec: gatewayClassName: kgateway tls: frontend: default: validation: mode: AllowValidOnly caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" perPort: - port: 8444 tls: validation: mode: AllowInsecureFallback caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" listeners: - name: https-8443 protocol: HTTPS port: 8443 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All - name: https-8444 protocol: HTTPS port: 8444 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All EOFIf you have not done so yet, get the external address of the gateway and save it in an environment variable. Note that it might take a few seconds for the gateway address to become available.
export INGRESS_GW_ADDRESS=$(kubectl get svc -n kgateway-system mtls -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo $INGRESS_GW_ADDRESSSend a request to the httpbin app on both ports without a valid certificate. Verify that the request on port 8443 fails, because the default validation configuration does not allow to establish a connection without a valid certificate. The connection on port 8444 however is established as the port-specific validation configuration mode is set to
AllowInsecureFallback.curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get curl -v -k --resolve "example.com:8444:${INGRESS_GW_ADDRESS}" https://example.com:8444/getExample output for port 8443:
* LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0 * Failed receiving HTTP2 data: 56(Failure when receiving data from the peer) * Connection #0 to host localhost left intact curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0Example output for port 8444:
... * Request completely sent off < HTTP/2 200 ...Repeat the request with a valid certificate. Verify that both requests succeed.
curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pem curl -v -k --resolve "example.com:8444:${INGRESS_GW_ADDRESS}" https://example.com:8444/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pem
Per listener configuration
Use the clientCertificateValidation setting in a ListenerPolicy to validate client certificates for a single listener. The setting overrides the FrontendTLS validation configuration of the Gateway for that listener only. Listeners that are not targeted by a ListenerPolicy continue to use the default or perPort configuration of the Gateway.
The FrontendTLS default and perPort configurations apply at the port level. When multiple listeners share the same port, the CA certificates of that port are combined into a single trust pool. As a result, any client certificate that is signed by any of these CAs is valid for every listener on that port. To give each listener its own trust boundary, target a single listener with sectionName. The gateway selects the listener, and therefore the CA, by using the Server Name Indication (SNI) that the client sends in the TLS handshake.
This setup is common in multi-tenant environments where each tenant has its own hostname and its own CA, but all tenants must be served by a single Gateway, and therefore a single load balancer.
Important
Per-listener validation can be bypassed when a wildcard listener, such as *.example.com, overlaps a more specific hostname on the same port. Because TLS connections to a port can be coalesced, a client that is validated against the CA of the wildcard listener can reuse that connection to reach the more specific listener. Use per-listener validation only if the hostnames of the listeners on a port do not overlap. For more information, see GEP-91 and GEP-3567.
The validation modes of a ListenerPolicy differ from the FrontendTLS validation modes of a Gateway. A Gateway sets spec.tls.frontend.default.validation.mode to AllowValidOnly or AllowInsecureFallback. A ListenerPolicy sets spec.default.clientCertificateValidation.mode to Require or Optional.
- Require: A connection can only be established if the client presents a valid certificate. This mode is equivalent to the
AllowValidOnlymode of a Gateway. - Optional: A connection can be established without a client certificate. However, if a client presents a certificate that cannot be validated, the connection is rejected. This mode is not equivalent to the
AllowInsecureFallbackmode of a Gateway. TheAllowInsecureFallbackmode establishes the connection even if the certificate is invalid.
In this example, you serve two tenants from two listeners that share port 8443, and you isolate their CA certificates so that the client certificate of tenant A cannot be used to access the listener of tenant B.
Navigate to the
example_certsdirectory that you created in Create TLS certificates, and create a separate CA and client certificate for each tenant.# Create the CA and client certificate for tenant A openssl genrsa -out tenant-a-ca-key.pem 2048 openssl req -new -x509 -days 365 -key tenant-a-ca-key.pem -out tenant-a-ca-cert.pem \ -subj "/CN=Tenant A CA/O=Tenant A" openssl genrsa -out tenant-a-client-key.pem 2048 openssl req -new -key tenant-a-client-key.pem -out tenant-a-client.csr \ -subj "/CN=client.tenant-a.example.com/O=Tenant A" openssl x509 -req -days 365 -in tenant-a-client.csr \ -CA tenant-a-ca-cert.pem -CAkey tenant-a-ca-key.pem \ -CAcreateserial -out tenant-a-client-cert.pem # Create the CA and client certificate for tenant B openssl genrsa -out tenant-b-ca-key.pem 2048 openssl req -new -x509 -days 365 -key tenant-b-ca-key.pem -out tenant-b-ca-cert.pem \ -subj "/CN=Tenant B CA/O=Tenant B" openssl genrsa -out tenant-b-client-key.pem 2048 openssl req -new -key tenant-b-client-key.pem -out tenant-b-client.csr \ -subj "/CN=client.tenant-b.example.com/O=Tenant B" openssl x509 -req -days 365 -in tenant-b-client.csr \ -CA tenant-b-ca-cert.pem -CAkey tenant-b-ca-key.pem \ -CAcreateserial -out tenant-b-client-cert.pemStore the CA certificate of each tenant in a separate ConfigMap. The gateway proxy later uses these certificates to validate the client certificate that is presented on the listener of that tenant. You can also store the CA certificates in Secrets. In both cases, the data key must be
ca.crt.kubectl create configmap tenant-a-ca-cert \ --from-file=ca.crt=tenant-a-ca-cert.pem \ -n kgateway-system kubectl create configmap tenant-b-ca-cert \ --from-file=ca.crt=tenant-b-ca-cert.pem \ -n kgateway-systemUpdate your Gateway to serve both tenants from port 8443. Each listener has its own hostname. The
certificateRefsfield sets the server certificate that the gateway presents to clients. Both listeners use the samehttps-certcertificate, because it includes the*.example.comSubject Alternative Name. The FrontendTLSdefaultconfiguration references theca-certCA. This CA validates client certificates on any listener that a ListenerPolicy does not target.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: mtls namespace: kgateway-system spec: gatewayClassName: kgateway tls: frontend: default: validation: mode: AllowValidOnly caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" listeners: - name: tenant-a protocol: HTTPS port: 8443 hostname: tenant-a.example.com tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All - name: tenant-b protocol: HTTPS port: 8443 hostname: tenant-b.example.com tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret allowedRoutes: namespaces: from: All EOFCreate an HTTPRoute for each tenant that routes to the httpbin app.
kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: httpbin-tenant-a namespace: httpbin labels: example: httpbin-route spec: hostnames: - "tenant-a.example.com" parentRefs: - name: mtls namespace: kgateway-system sectionName: tenant-a rules: - backendRefs: - name: httpbin port: 8000 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: httpbin-tenant-b namespace: httpbin labels: example: httpbin-route spec: hostnames: - "tenant-b.example.com" parentRefs: - name: mtls namespace: kgateway-system sectionName: tenant-b rules: - backendRefs: - name: httpbin port: 8000 EOFCreate a ListenerPolicy for each listener. Each policy uses the
sectionNamefield to target a single listener on the Gateway, and overrides the CA certificate that this listener uses to validate client certificates.kubectl apply -f- <<EOF apiVersion: gateway.kgateway.dev/v1alpha1 kind: ListenerPolicy metadata: name: tenant-a-mtls namespace: kgateway-system spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: mtls sectionName: tenant-a default: clientCertificateValidation: mode: Require caCertificateRefs: - name: tenant-a-ca-cert kind: ConfigMap group: "" --- apiVersion: gateway.kgateway.dev/v1alpha1 kind: ListenerPolicy metadata: name: tenant-b-mtls namespace: kgateway-system spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: mtls sectionName: tenant-b default: clientCertificateValidation: mode: Require caCertificateRefs: - name: tenant-b-ca-cert kind: ConfigMap group: "" EOFReview the following table to understand this configuration. For more information, see the API docs.
Setting Description targetRefs.sectionNameThe name of the listener on the Gateway that this policy applies to. If you omit this field, the policy applies to all listeners on the Gateway. clientCertificateValidation.modeHow client certificate validation is enforced on this listener. Set to RequireorOptional.clientCertificateValidation.caCertificateRefsReferences to the Secrets or ConfigMaps that contain the CA certificates that validate client certificates on this listener. Each reference must contain a ca.crtkey with the PEM data. You can add up to 8 references, which are combined into a single trust pool for this listener. These CA certificates are separate from the server certificate that the listener presents to clients, which you set in thecertificateRefsfield of the Gateway listener.Note
If the CA certificate is in a different namespace than the Gateway, you must create a ReferenceGrant. In the
fromsection of the ReferenceGrant, refer to the Gateway or ListenerSet that owns the listener, not to the ListenerPolicy. For more information, see Cross-namespace references.If you have not done so yet, get the external address of the gateway and save it in an environment variable. Note that it might take a few seconds for the gateway address to become available.
export INGRESS_GW_ADDRESS=$(kubectl get svc -n kgateway-system mtls -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo $INGRESS_GW_ADDRESSSend a request to the listener of tenant A with the client certificate of tenant A. Verify that the request succeeds.
curl -v -k --resolve "tenant-a.example.com:8443:${INGRESS_GW_ADDRESS}" https://tenant-a.example.com:8443/get \ --cert tenant-a-client-cert.pem \ --key tenant-a-client-key.pem \ --cacert ca-cert.pemExample output:
... * Request completely sent off < HTTP/2 200 ...Repeat the request to the listener of tenant B, but continue to use the client certificate of tenant A. Verify that the request fails, because the listener of tenant B only trusts the CA of tenant B. Without per-listener validation, this request would succeed, because both CAs would be part of the trust pool of port 8443.
curl -v -k --resolve "tenant-b.example.com:8443:${INGRESS_GW_ADDRESS}" https://tenant-b.example.com:8443/get \ --cert tenant-a-client-cert.pem \ --key tenant-a-client-key.pem \ --cacert ca-cert.pemExample output:
* LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C418:SSL routines:ST_OK:tlsv1 alert unknown ca, errno 0 * Failed receiving HTTP2 data: 56(Failure when receiving data from the peer) * Connection #0 to host tenant-b.example.com left intact curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C418:SSL routines:ST_OK:tlsv1 alert unknown ca, errno 0Repeat the request to the listener of tenant B with the client certificate of tenant B. Verify that the request succeeds.
curl -v -k --resolve "tenant-b.example.com:8443:${INGRESS_GW_ADDRESS}" https://tenant-b.example.com:8443/get \ --cert tenant-b-client-cert.pem \ --key tenant-b-client-key.pem \ --cacert ca-cert.pemExample output:
... * Request completely sent off < HTTP/2 200 ...
Additional TLS settings
You can configure your mTLS listener to limit connections to clients that present a certificate with a specific certificate hash and Subject Alternative Names. Alternatively, you can configure your listeners to enforce other TLS settings, such as a minimum or maximum TLS version, specific cipher suites, or ECDH curves. For more information, see Additional TLS settings.
Get the certificate hash of the client certificate.
openssl x509 -in client-cert.pem -noout -fingerprint -sha256Example output:
sha256 Fingerprint=46:DB:0D:C2:E1:4F:0A:05:8C:4F:05:8D:77:B1:8D:7C:1A:BE:18:4F:AF:81:BF:E2:B1:CD:03:43:7F:D8:65:4BUpdate port 8443 on the Gateway to allow connections only if the client certificate includes a specific hash and Subject Alt Name.
kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: mtls namespace: kgateway-system spec: gatewayClassName: kgateway tls: frontend: default: validation: mode: AllowValidOnly caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" listeners: - name: https-8443 protocol: HTTPS port: 8443 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret options: kgateway.dev/verify-subject-alt-names: "example.com" kgateway.dev/verify-certificate-hash: "46:DB:0D:C2:E1:4F:0A:05:8C:4F:05:8D:77:B1:8D:7C:1A:BE:18:4F:AF:81:BF:E2:B1:CD:03:43:7F:D8:65:4B" allowedRoutes: namespaces: from: All EOFSetting Description kgateway.dev/verify-certificate-hashA comma-delimited list of the certificate hash (fingerprint) that must be present in the peer certificate that is presented during the TLS handshake. kgateway.dev/verify-subject-alt-namesA comma-delimited list of the Subject Alternative Names that must be present in the peer certificate that is presented during the TLS handshake. Send a request to the httpbin app with your client certificate. Verify that the request succeeds.
curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pemExample output:
* Added example.com:8443:172.18.0.10 to DNS cache * Hostname example.com was found in DNS cache * Trying 172.18.0.10:8443... * Connected to example.com (172.18.0.10) port 8443 * ALPN: curl offers h2,http/1.1 * (304) (OUT), TLS handshake, Client hello (1): * (304) (IN), TLS handshake, Server hello (2): * (304) (IN), TLS handshake, Unknown (8): * (304) (IN), TLS handshake, Request CERT (13): * (304) (IN), TLS handshake, Certificate (11): * (304) (IN), TLS handshake, CERT verify (15): * (304) (IN), TLS handshake, Finished (20): * (304) (OUT), TLS handshake, Certificate (11): * (304) (OUT), TLS handshake, CERT verify (15): * (304) (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF * ALPN: server accepted h2 * Server certificate: * subject: CN=example.com; O=Test Org * start date: Jan 14 16:29:20 2026 GMT * expire date: Jan 14 16:29:20 2027 GMT * issuer: CN=Test CA; O=Test Org * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://example.com:8443/get * [HTTP/2] [1] [:method: GET] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: example.com:8443] * [HTTP/2] [1] [:path: /get] * [HTTP/2] [1] [user-agent: curl/8.7.1] * [HTTP/2] [1] [accept: */*] > GET /get HTTP/2 > Host: example.com:8443 > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/2 200 ...Change the annotation values in the Gateway to a value that does not exist in the client certificate. For example, you can change the Subject Alt Name to
mtls.com.kubectl apply -f- <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: mtls namespace: kgateway-system spec: gatewayClassName: kgateway tls: frontend: default: validation: mode: AllowValidOnly caCertificateRefs: - name: ca-cert kind: ConfigMap group: "" listeners: - name: https-8443 protocol: HTTPS port: 8443 tls: mode: Terminate certificateRefs: - name: https-cert kind: Secret options: kgateway.dev/verify-subject-alt-names: "mtls.com" kgateway.dev/verify-certificate-hash: "46:DB:0D:C2:E1:4F:0A:05:8C:4F:05:8D:77:B1:8D:7C:1A:BE:18:4F:AF:81:BF:E2:B1:CD:03:43:7F:D8:65:4B" allowedRoutes: namespaces: from: All EOFRepeat the request to the httpbin app with your client certificate. Verify that the request now fails.
curl -v -k --resolve "example.com:8443:${INGRESS_GW_ADDRESS}" https://example.com:8443/get \ --cert client-cert.pem \ --key client-key.pem \ --cacert ca-cert.pemExample output:
* LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0 * Failed receiving HTTP2 data: 56(Failure when receiving data from the peer) * Connection #0 to host localhost left intact curl: (56) LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
Cleanup
You can remove the resources that you created in this guide.kubectl delete httproute httpbin-https httpbin-tenant-a httpbin-tenant-b -n httpbin
kubectl delete listenerpolicy tenant-a-mtls tenant-b-mtls -n kgateway-system
kubectl delete configmap tenant-a-ca-cert tenant-b-ca-cert -n kgateway-system
kubectl delete gateway mtls -n kgateway-system
kubectl delete secret https-cert -n kgateway-system
kubectl delete configmap ca-cert -n kgateway-system
rm -rf ../example_certs