Skip to content

Tier 2: Authenticate clients with provider-specific TLS validation

When to use this approach

Use Tier 2 X.509 authentication when you need defense-in-depth security but don't have Gateway API v1.5+ yet:

  • Defense-in-depth security: Same two-layer validation as Tier 1 (TLS + Application)
  • Older Gateway API versions: Works with Gateway API versions prior to v1.5
  • Provider flexibility: Supports Istio (via EnvoyFilter) and Envoy Gateway (via EnvoyPatchPolicy)
  • Migration path: Clear upgrade path to Tier 1 when you adopt Gateway API v1.5+

This approach provides the same security guarantees as Tier 1 but requires provider-specific configuration knowledge.

How it works

Tier 2 implements the same two-layer validation model as Tier 1, but uses provider-specific resources for TLS configuration:

Layer 1 (TLS/L4): Gateway validates client certificates during TLS handshake

  • Configured via EnvoyFilter (Istio) or EnvoyPatchPolicy (Envoy Gateway)
  • Client presents certificate during mTLS handshake
  • Gateway validates against configured CA certificates
  • Invalid, expired, or untrusted certificates are rejected
  • Gateway sets x-forwarded-client-cert (XFCC) header with certificate details
  • Incoming XFCC headers from clients are sanitized

Layer 2 (Application/L7): Authorino validates certificates from XFCC header

  • Identical to Tier 1: AuthPolicy extracts certificate from XFCC header
  • Applies fine-grained validation using label selectors on CA Secrets
  • Enables multi-CA trust scenarios

Result: Request proceeds only if both layers succeed.

Before you begin

Ensure you have:

  • Kubernetes cluster: Any supported version
  • Gateway API: Any version (does not require v1.5)
  • Gateway implementation: Istio (any version) or Envoy Gateway
  • Kuadrant Operator: Installed with Kuadrant instance deployed
  • Provider knowledge: Familiarity with EnvoyFilter (Istio) or EnvoyPatchPolicy (Envoy Gateway)

Choose your provider

Select the guide for your gateway provider:

Both approaches achieve the same security outcome and use identical AuthPolicy configuration.


Istio: EnvoyFilter configuration

Step 1: Prepare CA and client certificates

Generate or obtain CA and client certificates. Client certificates must include extendedKeyUsage=clientAuth.

# Generate CA
openssl req -x509 -sha512 -nodes \
  -days 365 \
  -newkey rsa:4096 \
  -subj "/CN=Test CA/O=Kuadrant/C=US" \
  -addext basicConstraints=CA:TRUE \
  -addext keyUsage=digitalSignature,keyCertSign \
  -keyout /tmp/ca.key \
  -out /tmp/ca.crt

# Create X.509 v3 extensions for client certificate
cat > /tmp/x509v3.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
extendedKeyUsage=clientAuth
EOF

# Generate client certificate
openssl genrsa -out /tmp/client.key 4096
openssl req -new \
  -subj "/CN=test-client/O=Kuadrant/C=US" \
  -key /tmp/client.key \
  -out /tmp/client.csr
openssl x509 -req -sha512 \
  -days 365 \
  -CA /tmp/ca.crt \
  -CAkey /tmp/ca.key \
  -CAcreateserial \
  -extfile /tmp/x509v3.ext \
  -in /tmp/client.csr \
  -out /tmp/client.crt

Important

Client certificates must include extendedKeyUsage=clientAuth for Authorino validation to succeed.

Step 2: Create CA certificate resources

# ConfigMap for Gateway TLS validation (Layer 1)
kubectl create configmap client-ca-bundle \
  -n gateway-system \
  --from-file=ca.crt=/tmp/ca.crt

# Secret for Authorino validation (Layer 2)
kubectl create secret tls trusted-client-ca \
  -n kuadrant-system \
  --cert=/tmp/ca.crt \
  --key=/tmp/ca.key

# Label the secret so Authorino can discover it
kubectl label secret trusted-client-ca \
  -n kuadrant-system \
  authorino.kuadrant.io/managed-by=authorino \
  app.kubernetes.io/name=trusted-client

Step 3: Configure Gateway

Create a Gateway without frontend TLS validation (handled by EnvoyFilter instead):

kubectl apply -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/refs/heads/main/examples/x509-authentication/gateway-tier2.yaml

This creates:

  • cert-manager Issuer: Self-signed certificate issuer for the gateway server certificate
  • TLSPolicy: Kuadrant policy to manage the gateway's server TLS certificate
  • Gateway: Standard Gateway without spec.tls.frontend.default.validation (client certificate validation handled by EnvoyFilter)

Step 4: Mount CA certificate into gateway pods

Patch the gateway deployment to mount the CA certificate Secret:

Warning

This approach directly patches the gateway deployment, which is managed by the gateway controller. In production environments, use proper configuration mechanisms (e.g., Istio Helm values or IstioOperator overlays) to ensure changes persist across controller reconciliations.

kubectl patch deployment -n gateway-system \
  $(kubectl get deployment -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway -o jsonpath='{.items[0].metadata.name}') \
  --type=json -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/volumes/-",
    "value": {
      "name": "client-ca-bundle",
      "configMap": {
        "name": "client-ca-bundle"
      }
    }
  },
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/volumeMounts/-",
    "value": {
      "name": "client-ca-bundle",
      "mountPath": "/etc/certs",
      "readOnly": true
    }
  }
]'

Step 5: Create EnvoyFilter for mTLS validation

Create an EnvoyFilter to configure Envoy's DownstreamTlsContext for client certificate validation:

kubectl apply -f -<<EOF
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: mtls-validation
  namespace: gateway-system
spec:
  targetRefs:

  - group: gateway.networking.k8s.io
    kind: Gateway
    name: mtls-gateway
  configPatches:
  - applyTo: FILTER_CHAIN
    match:
      context: GATEWAY
      listener:
        portNumber: 443
    patch:
      operation: MERGE
      value:
        transport_socket:
          name: envoy.transport_sockets.tls
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
            commonTlsContext:
              validationContext:
                trustedCa:
                  filename: /etc/certs/ca.crt
            requireClientCertificate: true
EOF

Step 6: Deploy application and create HTTPRoute

# Deploy httpbin application
kubectl apply -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/refs/heads/main/examples/x509-authentication/httpbin.yaml

# Create HTTPRoute
kubectl apply -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/refs/heads/main/examples/x509-authentication/httproute.yaml

Step 7: Configure AuthPolicy

The AuthPolicy configuration is identical to Tier 1 - it extracts the certificate from the XFCC header and validates it:

kubectl apply -f https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/refs/heads/main/examples/x509-authentication/authpolicy.yaml

This creates an AuthPolicy that:

  • Extracts the certificate from the x-forwarded-client-cert header
  • Validates the certificate chain against CA certificates labeled app.kubernetes.io/name: trusted-client
  • Enforces authorization based on certificate Organization attribute
  • Injects certificate attributes into request headers

Envoy Gateway: EnvoyPatchPolicy configuration

Step 1: Prepare CA and client certificates

Same as Istio (see Istio Step 1)

Step 2: Create CA certificate resources

Same as Istio (see Istio Step 2)

Step 3: Configure Gateway

Similar to Istio (see Istio Step 3), but adjust gatewayClassName for Envoy Gateway.

Step 4: Mount CA certificate into gateway pods

Patch the gateway deployment to mount the CA certificate Secret:

Warning

This approach directly patches the gateway deployment, which is managed by the Envoy Gateway controller. In production environments, use proper configuration mechanisms (e.g., EnvoyProxy resource with extraVolumes) to ensure changes persist across controller reconciliations.

kubectl patch deployment -n gateway-system \
  $(kubectl get deployment -n gateway-system -l gateway.envoyproxy.io/owning-gateway-name=mtls-gateway -o jsonpath='{.items[0].metadata.name}') \
  --type=json -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/volumes/-",
    "value": {
      "name": "client-ca-bundle",
      "configMap": {
        "name": "client-ca-bundle"
      }
    }
  },
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/volumeMounts/-",
    "value": {
      "name": "client-ca-bundle",
      "mountPath": "/etc/certs",
      "readOnly": true
    }
  }
]'

Step 5: Create EnvoyPatchPolicy for mTLS validation

Create an EnvoyPatchPolicy to configure client certificate validation:

kubectl apply -f -<<EOF
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyPatchPolicy
metadata:
  name: mtls-validation
  namespace: gateway-system
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: mtls-gateway
  type: JSONPatch
  jsonPatches:

  - type: "type.googleapis.com/envoy.config.listener.v3.Listener"
    name: https
    operation:
      op: add
      path: "/filter_chains/0/transport_socket/typed_config/commonTlsContext/validationContext"
      value:
        trustedCa:
          filename: /etc/certs/ca.crt
  - type: "type.googleapis.com/envoy.config.listener.v3.Listener"
    name: https
    operation:
      op: add
      path: "/filter_chains/0/transport_socket/typed_config/requireClientCertificate"
      value: true
EOF

Step 6: Deploy application and create HTTPRoute

Same as Istio (see Istio Step 6)

Step 7: Configure AuthPolicy

Same as Istio (see Istio Step 7)


Verify defense-in-depth security

Testing is identical to Tier 1. Run the same test scenarios:

GATEWAY_IP=$(kubectl get gateway mtls-gateway -n gateway-system -o jsonpath='{.status.addresses[0].value}')

# Test 1: Valid certificate → HTTP 200
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/client.crt \
  --key /tmp/client.key

# Test 2: No certificate → TLS handshake fails
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get

# Test 3: Untrusted certificate → TLS handshake fails
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/untrusted.crt \
  --key /tmp/untrusted.key

# Test 4: Valid cert, wrong attributes → HTTP 403
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/unauthorized-client.crt \
  --key /tmp/unauthorized-client.key

Migration to Tier 1

When you upgrade to Gateway API v1.5+ and your gateway provider supports spec.tls.frontend.default.validation, migrate from Tier 2 to Tier 1:

Migration steps

  1. Update Gateway resource to include frontend TLS validation:

    spec:
      tls:
        frontend:
          default:
            validation:
              caCertificateRefs:
              - name: client-ca-bundle
                kind: ConfigMap
              mode: AllowValidOnly
    

  2. Remove provider-specific resource:

    # For Istio
    kubectl delete envoyfilter mtls-validation -n gateway-system
    
    # For Envoy Gateway
    kubectl delete envoypatchpolicy mtls-validation -n gateway-system
    

  3. Keep AuthPolicy unchanged: AuthPolicy configuration remains identical

  4. Verify: Run the same test scenarios to confirm migration succeeded

Why migrate?

  • Standardization: Uses Gateway API standard instead of vendor-specific resources
  • Maintainability: Simpler configuration, easier to understand
  • Portability: Gateway API configuration is portable across providers
  • Future-proof: Gateway API v1.5 is the recommended approach going forward

Troubleshooting

EnvoyFilter not applied

Symptoms: TLS handshake succeeds without client certificate

Resolution:

# Verify EnvoyFilter exists
kubectl get envoyfilter mtls-validation -n gateway-system

# Check targetRef points to gateway
kubectl get envoyfilter mtls-validation -n gateway-system -o yaml | grep -A 3 targetRefs

# Check Envoy configuration was patched
kubectl exec -n gateway-system \
  $(kubectl get pod -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway -o jsonpath='{.items[0].metadata.name}') \
  -- curl -s localhost:15000/config_dump?include_eds | grep -A 20 validation_context

CA certificate not mounted

Symptoms: Gateway logs show "failed to load trusted CA"

Resolution:

# Verify ConfigMap exists
kubectl get configmap client-ca-bundle -n gateway-system

# Check volume mount in gateway pod
kubectl describe pod -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway | grep -A 5 Mounts

# Verify file exists in pod
kubectl exec -n gateway-system \
  $(kubectl get pod -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway -o jsonpath='{.items[0].metadata.name}') \
  -- cat /etc/certs/ca.crt

XFCC header not set correctly

Symptoms: Authorino rejects with "certificate not found"

Resolution:

# Verify forward_client_cert_details configuration
kubectl exec -n gateway-system \
  $(kubectl get pod -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway -o jsonpath='{.items[0].metadata.name}') \
  -- curl -s localhost:15000/config_dump?include_eds | grep -i forward_client_cert

# Check Envoy access logs for XFCC
kubectl logs -n gateway-system -l gateway.networking.k8s.io/gateway-name=mtls-gateway | grep XFCC

See also