mTLS (FrontendTLS)

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. Note that you must install version 1.5 or later of the Kubernetes Gateway API to use these capabilities.

  • 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.

About this guide

In this guide, you learn how to apply default certificate validation configuration for all HTTPS listeners on a Gateway and how to override this configuration for a specific port. 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.

⚠️
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

  1. Deploy the httpbin sample app.

  2. Make sure that you have the OpenSSL version of openssl, not LibreSSL. The openssl version must be at least 1.1.

    1. Check the openssl version that is installed. If you see LibreSSL in the output, continue to the next step.

      openssl version
    2. Install the OpenSSL version (not LibreSSL). For example, you might use Homebrew.

      brew install openssl
    3. Review 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...
  3. Install the experimental channel of the Kubernetes Gateway API. This API is required to use the FrontendTLS configuration on a Gateway.

    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
  4. Ensure that you installed agentgateway with the --set controller.extraEnv.KGW_ENABLE_GATEWAY_API_EXPERIMENTAL_FEATURES=true Helm flag to use experimental Kubernetes Gateway API features. For an example, see the Get started guide.

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.

⚠️
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 trust Certificate Authority.
  1. Create the example_certs directory and navigate to this directory.

    mkdir example_certs && cd example_certs
  2. Create 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"
  3. 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")
  4. 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 agentgateway-system
  5. Store 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 agentgateway-system
  6. Create 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")
  7. Continue with configuring a Default configuration for all listeners. Alternatively, you can explore how to override the default configuration for a specific port.

Default configuration for all listeners

Create the default client certificate validation configuration for all Gateway listeners that handle HTTPS traffic. In this example, the configuration is applied to two TLS ports, 8443 and 8444.

  1. 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 AllowValidOnly to 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: agentgateway-system
    spec:
      gatewayClassName: agentgateway
      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
    EOF
  2. Create an HTTPRoute that routes incoming traffic on the example.com domain 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: agentgateway-system
      rules:
        - backendRefs:
            - name: httpbin
              port: 8000
    EOF
  3. 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 agentgateway-system mtls -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS   
    kubectl port-forward deploy/mtls -n agentgateway-system 8443:8443 8444:8444

  4. Send 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/get
    curl -v -k --resolve "example.com:8443:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8443/get
    
    curl -v -k --resolve "example.com:8444:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8444/get
    curl -v -k https://localhost:8443/get \
      --resolve example.com:8443:127.0.0.1 \
      -H "Host: example.com"
      
    curl -v -k https://localhost:8444/get \
      --resolve example.com:8444:127.0.0.1 \
      -H "Host: example.com"

    Example 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
  5. Repeat 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.pem
    curl -v -k --resolve "example.com:8443:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8443/get \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    
    curl -v -k --resolve "example.com:8444:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8444/get \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    curl -v -k https://localhost:8443/get \
      --resolve example.com:8443:127.0.0.1 \
      -H "Host: example.com" \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    
    curl -v -k https://localhost:8444/get \
      --resolve example.com:8444:127.0.0.1 \
      -H "Host: example.com" \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem

    Example 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.

  1. 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: agentgateway-system
    spec:
      gatewayClassName: agentgateway
      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
    EOF
  2. 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 agentgateway-system mtls -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS   
    kubectl port-forward deploy/mtls -n agentgateway-system 8443:8443 8444:8444

  3. Send 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 you to establish a connection without a valid certificate. However, the connection on port 8444 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/get
    curl -v -k --resolve "example.com:8443:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8443/get
    
    curl -v -k --resolve "example.com:8444:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8444/get
    curl -v -k https://localhost:8443/get \
      --resolve example.com:8443:127.0.0.1 \
      -H "Host: example.com"
      
    curl -v -k https://localhost:8444/get \
      --resolve example.com:8444:127.0.0.1 \
      -H "Host: example.com"

    Example 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 0

    Example output for port 8444:

    ...
    * Request completely sent off
    < HTTP/2 200 
    ...
  4. 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
    curl -v -k --resolve "example.com:8443:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8443/get \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    
    curl -v -k --resolve "example.com:8444:$(dig +short $INGRESS_GW_ADDRESS | head -n1)"  https://example.com:8444/get \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    curl -v -k https://localhost:8443/get \
      --resolve example.com:8443:127.0.0.1 \
      -H "Host: example.com" \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem
    
    curl -v -k https://localhost:8444/get \
      --resolve example.com:8444:127.0.0.1 \
      -H "Host: example.com" \
      --cert client-cert.pem \
      --key client-key.pem \
      --cacert ca-cert.pem

Cleanup

You can remove the resources that you created in this guide.
kubectl delete httproute httpbin-https -n httpbin
kubectl delete gateway mtls -n agentgateway-system
kubectl delete secret https-cert -n agentgateway-system
kubectl delete configmap ca-cert -n agentgateway-system
rm -rf ../example_certs
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

↑↓ navigate select esc dismiss

What could be improved?