Skip to content

Tier 1: Authenticate clients with Gateway API frontend TLS validation

When to use this approach

Use Tier 1 X.509 authentication when you have Gateway API v1.5+ and want the most secure, standards-based mTLS authentication with:

  • Defense-in-depth security: Validation at both TLS (L4) and application (L7) layers
  • Cryptographic proof: Gateway verifies client possesses the private key
  • Standard configuration: No vendor-specific resources required
  • Automatic protection: Gateway sanitizes XFCC headers to prevent spoofing

This is the recommended approach for all Gateway API v1.5+ deployments.

How it works

Tier 1 implements two-layer validation for defense-in-depth security:

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

  • Client presents certificate during mTLS handshake
  • Gateway validates against CAs configured in spec.tls.frontend.default.validation.caCertificateRefs
  • Invalid, expired, or untrusted certificates are rejected
  • Gateway sets x-forwarded-client-cert (XFCC) header with certificate details
  • Incoming XFCC headers from clients are automatically stripped

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

  • Wasm-shim forwards XFCC header to Authorino
  • Authorino 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 1.31+: Required for Gateway API v1.5 CEL validation rules
  • Gateway API v1.5+: Must support spec.tls.frontend.default.validation
  • Compatible Gateway implementation:
  • Istio v1.28+ (Sail Operator v1.28.0+ or upstream Istio via istioctl)
  • Envoy Gateway with v1.5 support
  • ⚠️ Istio 1.27.x and earlier do NOT support spec.tls.frontend
  • Kuadrant Operator: Installed with Kuadrant instance deployed

Step-by-step walkthrough

Step 1: Prepare CA and client certificates

For testing, generate self-signed certificates. For production, use certificates from your PKI or cert-manager.

# Generate CA private key and certificate
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 file for client certificate
cat > /tmp/x509v3.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
extendedKeyUsage=clientAuth
EOF

# Generate client private key and certificate signed by the CA
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

Create the CA certificate as a ConfigMap for gateway validation and as a Secret for Authorino validation:

# 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

Why two resources?

  • ConfigMap: Gateway reads CA certificates from ConfigMaps for TLS validation
  • Secret: Authorino reads CA certificates from labeled TLS Secrets for application-layer validation

Step 3: Configure Gateway with frontend TLS validation

Create a Gateway with spec.tls.frontend.default.validation to enable client certificate validation at the TLS layer:

Note

The example file below also includes a cert-manager Issuer and TLSPolicy for automated server certificate provisioning. The Gateway configuration shown here highlights the key mTLS validation settings.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mtls-gateway
  namespace: gateway-system
spec:
  gatewayClassName: istio
  listeners:

  - name: https
    protocol: HTTPS
    port: 443
    hostname: "*.nip.io"
    allowedRoutes:
      namespaces:
        from: All
    tls:
      mode: Terminate
      certificateRefs:
      - name: gateway-tls-cert  # Server certificate for TLS termination
        kind: Secret
  # Frontend TLS validation for client certificates
  tls:
    frontend:
      default:
        validation:
          # Reference ConfigMap with trusted CA certificates
          caCertificateRefs:
          - name: client-ca-bundle
            kind: ConfigMap
            group: ""
          # Require valid client certificates (reject invalid ones at TLS layer)
          mode: AllowValidOnly  # or AllowInsecureFallback for optional mTLS

Key configuration:

  • caCertificateRefs: References the ConfigMap containing CA certificates in PEM format
  • mode: AllowValidOnly: Requires valid client certificates; connections without certificates or with invalid certificates are rejected during TLS handshake
  • mode: AllowInsecureFallback: Allows connections without client certificates (optional mTLS)

Apply the Gateway:

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

What happens: Gateway now validates client certificates during TLS handshake and sets the XFCC header for valid connections.

Step 4: Deploy the protected application

Deploy a test application to protect with X.509 authentication:

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

This creates a Deployment and Service for the httpbin test application.

Step 5: Create an HTTPRoute

Route traffic from the Gateway to the application:

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

Example HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-route
  namespace: default
spec:
  parentRefs:

  - name: mtls-gateway
    namespace: gateway-system
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: httpbin
      port: 80

Step 6: Configure AuthPolicy for L7 validation

Create an AuthPolicy that extracts the certificate from the XFCC header and validates it using label selectors:

apiVersion: kuadrant.io/v1
kind: AuthPolicy
metadata:
  name: x509-auth-policy
  namespace: default
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: httpbin-route
  rules:
    # Authentication using client certificate from XFCC header
    authentication:
      "x509-client-cert":
        x509:
          # Extract certificate from XFCC header set by gateway
          source:
            xfccHeader: "x-forwarded-client-cert"
          # Select trusted CA certificates using labels
          selector:
            matchLabels:
              app.kubernetes.io/name: trusted-client

    # Optional: Enforce authorization based on certificate attributes
    authorization:
      "verify-organization":
        patternMatching:
          patterns:

          - predicate: "size(auth.identity.Organization) > 0 && auth.identity.Organization[0] == 'Kuadrant'"

    # Optional: Inject certificate attributes into request headers
    response:
      success:
        headers:
          "x-client-common-name":
            plain:
              expression: auth.identity.CommonName
          "x-client-org":
            plain:
              expression: auth.identity.Organization[0]

Apply the AuthPolicy:

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

Configuration breakdown:

  • source.xfccHeader: Extracts certificate from the XFCC header (set by gateway after TLS validation)
  • selector.matchLabels: Selects CA Secrets with matching labels for trust validation
  • authorization: Optional CEL expressions to enforce attribute-based policies
  • response.success.headers: Optional certificate attribute injection into request headers

Verify defense-in-depth security

Test all authentication scenarios to confirm both validation layers work correctly.

Test 1: Valid certificate (both layers pass)

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

curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/client.crt \
  --key /tmp/client.key

Expected: HTTP 200 response. Request succeeds because:

  1. ✅ Layer 1: Gateway validates certificate during TLS handshake
  2. ✅ Layer 2: Authorino validates certificate from XFCC header

Test 2: No certificate (Layer 1 rejects)

curl -ik https://httpbin.$GATEWAY_IP.nip.io/get

Expected: TLS handshake fails. Connection rejected at Layer 1 because mode: AllowValidOnly requires a client certificate.

Test 3: Untrusted certificate (Layer 1 rejects)

# Generate self-signed certificate not signed by the CA
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout /tmp/untrusted.key -out /tmp/untrusted.crt -days 365 \
  -subj "/CN=untrusted-client/O=Untrusted/C=US"

# Try to connect
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/untrusted.crt \
  --key /tmp/untrusted.key

Expected: TLS handshake fails. Gateway rejects connection because certificate is not signed by a trusted CA.

Test 4: Valid certificate with unauthorized attributes (Layer 2 rejects)

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

# Try to connect
curl -ik https://httpbin.$GATEWAY_IP.nip.io/get \
  --cert /tmp/unauthorized-client.crt \
  --key /tmp/unauthorized-client.key

Expected: HTTP 403 Forbidden. Request succeeds at Layer 1 (valid certificate chain) but fails at Layer 2 because the authorization rule requires Organization == "Kuadrant".

What you can do next

Use certificate attributes for authorization

Extract certificate subject fields for authorization decisions:

authorization:
  "department-access":
    patternMatching:
      patterns:

      - predicate: "'Engineering' in auth.identity.OrganizationalUnit"
  "regional-access":
    patternMatching:
      patterns:
      - predicate: "auth.identity.Country[0] == 'US' && auth.identity.Province[0] == 'California'"

Available identity fields: CommonName, Country, Organization, OrganizationalUnit, Locality, Province, StreetAddress, PostalCode, SerialNumber

Inject certificate claims into request headers

Forward certificate attributes to your application:

response:
  success:
    headers:
      "x-client-subject":
        json:
          properties:
            cn:
              expression: auth.identity.CommonName
            org:
              expression: auth.identity.Organization[0]
            ou:
              expression: auth.identity.OrganizationalUnit

Implement multi-CA trust

Trust different CAs for different routes or environments:

# AuthPolicy for production routes
x509:
  selector:
    matchLabels:
      environment: production
      tier: customer-facing
---
# AuthPolicy for internal routes
x509:
  selector:
    matchLabels:
      environment: internal
      tier: employee

Create CA Secrets with appropriate labels:

kubectl label secret production-ca \
  -n kuadrant-system \
  environment=production \
  tier=customer-facing

kubectl label secret internal-ca \
  -n kuadrant-system \
  environment=internal \
  tier=employee

Automate certificate management with cert-manager

Use cert-manager to automate CA certificate provisioning and rotation:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: client-ca
  namespace: kuadrant-system
spec:
  secretName: trusted-client-ca
  commonName: "Kuadrant Client CA"
  isCA: true
  issuerRef:
    name: root-ca-issuer
    kind: Issuer

Add labels to the cert-manager Certificate resource:

spec:
  secretTemplate:
    labels:
      authorino.kuadrant.io/managed-by: authorino
      app.kubernetes.io/name: trusted-client

Topology diagram

The following diagram illustrates the component relationships and request flow:

%%{init: {'theme':'neutral'}}%%
graph TB
    Client([Client with<br/>X.509 Certificate])

    subgraph "gateway-system namespace"
        GW[Gateway<br/>mtls-gateway]
        CACM[(ConfigMap<br/>client-ca-bundle)]
        TLSCERT[(Secret<br/>gateway-tls-cert)]
    end

    subgraph "default namespace"
        HR[HTTPRoute<br/>httpbin-route]
        AP[AuthPolicy<br/>x509-auth-policy]
        SVC[Service<br/>httpbin]
        POD[Deployment<br/>httpbin]
    end

    subgraph "kuadrant-system namespace"
        CASEC[(Secret<br/>trusted-client-ca)]
    end

    Client -->|"1. mTLS handshake<br/>(client cert)"| GW
    GW -.->|validates against| CACM
    GW -.->|server cert| TLSCERT
    GW -->|"2. XFCC header"| HR
    HR -->|routes to| SVC
    SVC --> POD
    AP -.->|attached to| HR
    AP -.->|validates cert via<br/>label selector| CASEC

Troubleshooting

XFCC header not populated

Symptoms: Authorino rejects requests with "certificate not found" or "missing XFCC header"

Possible causes:

  • Gateway not configured with spec.tls.frontend.default.validation
  • Client didn't present certificate during TLS handshake
  • Gateway implementation doesn't support frontend TLS validation

Resolution:

# Verify Gateway configuration
kubectl get gateway mtls-gateway -n gateway-system -o yaml | grep -A 10 frontend

# Check Authorino logs for XFCC header
kubectl logs -n kuadrant-system -l authorino-resource-uid=<uid> | grep -i xfcc

# Test TLS handshake manually
openssl s_client -connect $GATEWAY_IP:443 \
  -cert /tmp/client.crt -key /tmp/client.key \
  -servername httpbin.$GATEWAY_IP.nip.io

Certificate validation fails at Layer 2

Symptoms: TLS handshake succeeds but Authorino returns 403

Possible causes:

  • CA Secret not labeled correctly
  • Label selector doesn't match any CA Secrets
  • Certificate missing extendedKeyUsage=clientAuth
  • Authorization rules reject certificate attributes

Resolution:

# Verify CA Secret labels
kubectl get secret trusted-client-ca -n kuadrant-system --show-labels

# Check if selector matches
kubectl get secrets -n kuadrant-system -l app.kubernetes.io/name=trusted-client

# Verify certificate has Client Auth EKU
openssl x509 -in /tmp/client.crt -noout -text | grep "TLS Web Client Authentication"

# Check Authorino logs
kubectl logs -n kuadrant-system -l authorino-resource-uid=<uid> | grep x509

Gateway rejects valid certificates

Symptoms: TLS handshake fails even with valid CA-signed certificate

Possible causes:

  • ConfigMap doesn't contain CA certificate in PEM format
  • Certificate chain incomplete
  • Certificate expired
  • Gateway implementation issue

Resolution:

# Verify ConfigMap contains PEM-encoded CA
kubectl get configmap client-ca-bundle -n gateway-system -o yaml

# Check certificate validity
openssl x509 -in /tmp/client.crt -noout -dates

# Verify certificate chain
openssl verify -CAfile /tmp/ca.crt /tmp/client.crt

# Check gateway logs
kubectl logs -n gateway-system -l istio=ingressgateway

See also