This document describes how to configure TLS for VirtualServices in the Envoy XDS Controller.
The Envoy XDS Controller supports TLS termination for VirtualServices. TLS configuration is specified in the spec.tlsConfig field of a VirtualService or VirtualServiceTemplate.
There are two modes for configuring TLS certificates:
secretRef) - explicitly reference a Kubernetes TLS secretautoDiscovery) - automatically find secrets based on domain annotationsUse secretRef when you want to explicitly specify which Kubernetes secret contains the TLS certificate:
apiVersion: envoy.kaasops.io/v1alpha1
kind: VirtualService
metadata:
name: my-service
annotations:
envoy.kaasops.io/node-id: my-node
spec:
listener:
name: https
tlsConfig:
secretRef:
name: my-tls-secret
namespace: my-namespace # optional, defaults to VirtualService namespace
virtualHost:
domains:
- example.com
routes:
- match:
prefix: "/"
route:
cluster: my-cluster
Use autoDiscovery when you want the controller to automatically find TLS secrets based on domain annotations:
apiVersion: envoy.kaasops.io/v1alpha1
kind: VirtualService
metadata:
name: my-service
annotations:
envoy.kaasops.io/node-id: my-node
spec:
listener:
name: https
tlsConfig:
autoDiscovery: true
virtualHost:
domains:
- example.com
- api.example.com
routes:
- match:
prefix: "/"
route:
cluster: my-cluster
For auto discovery to work, your TLS secrets must have the envoy.kaasops.io/domains annotation:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: my-namespace
annotations:
envoy.kaasops.io/domains: "example.com,api.example.com"
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-certificate>
tls.key: <base64-encoded-private-key>
Auto discovery supports wildcard domain matching. A secret annotated with *.example.com will match any subdomain:
apiVersion: v1
kind: Secret
metadata:
name: wildcard-tls
annotations:
envoy.kaasops.io/domains: "*.example.com"
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-certificate>
tls.key: <base64-encoded-private-key>
This secret will be used for api.example.com, www.example.com, etc.
When multiple TLS secrets exist for the same domain (e.g., in different namespaces), the controller uses a deterministic algorithm to select the best secret.
The algorithm evaluates candidates in the following order:
| Priority | Criterion | Description |
|---|---|---|
| 1 | Certificate Validity | Valid (not expired) certificates have highest priority |
| 2 | Namespace Preference | Secrets in the same namespace as VirtualService are preferred |
| 3 | Alphabetical Order | Tie-breaker: sorted by namespace, then by name |
Certificates are classified into three validity states:
| State | Description | Priority |
|---|---|---|
| Valid | Certificate NotAfter date is in the future |
Highest |
| Unknown | Certificate could not be parsed (zero NotAfter) |
Medium |
| Expired | Certificate NotAfter date is in the past |
Lowest |
Example 1: Valid certificate preferred over expired
Domain: example.com
VirtualService namespace: ns2
Secrets:
ns1/cert-a: valid, expires 2025-12-31
ns2/cert-b: expired, expired 2024-01-01
Result: ns1/cert-a (valid > expired, even though ns2 is preferred namespace)
Example 2: Same namespace preferred when both valid
Domain: example.com
VirtualService namespace: ns2
Secrets:
ns1/cert-a: valid, expires 2025-12-31
ns2/cert-b: valid, expires 2025-06-30
Result: ns2/cert-b (both valid, ns2 matches VirtualService namespace)
Example 3: Alphabetical fallback
Domain: example.com
VirtualService namespace: ns3
Secrets:
ns1/cert-a: valid, expires 2025-12-31
ns2/cert-b: valid, expires 2025-06-30
Result: ns1/cert-a (both valid, neither matches ns3, alphabetically ns1 < ns2)
Example 4: Three-tier priority
Domain: example.com
VirtualService namespace: ns2
Secrets:
ns1/cert-valid: valid, expires 2025-12-31
ns2/cert-unknown: unknown, could not parse certificate
ns3/cert-expired: expired, expired 2024-01-01
Result: ns1/cert-valid (valid > unknown > expired)
When a secret contains a certificate chain (multiple certificates), the controller uses the minimum NotAfter date from all certificates in the chain. This ensures that the most restrictive expiration is considered.
TLS secrets must meet the following requirements:
kubernetes.io/tls or Opaquetls.crt - PEM-encoded certificate (or certificate chain)tls.key - PEM-encoded private keyFor certificate chains, include certificates in the following order:
apiVersion: envoy.kaasops.io/v1alpha1
kind: VirtualService
metadata:
name: secure-api
annotations:
envoy.kaasops.io/node-id: production
spec:
listener:
name: https
tlsConfig:
secretRef:
name: api-tls-cert
virtualHost:
domains:
- api.example.com
routes:
- match:
prefix: "/"
route:
cluster: api-backend
apiVersion: envoy.kaasops.io/v1alpha1
kind: VirtualService
metadata:
name: multi-domain-service
annotations:
envoy.kaasops.io/node-id: production
spec:
listener:
name: https
tlsConfig:
autoDiscovery: true
virtualHost:
domains:
- www.example.com
- api.example.com
- admin.example.com
routes:
- match:
prefix: "/"
route:
cluster: main-backend
apiVersion: v1
kind: Secret
metadata:
name: example-com-tls
namespace: production
annotations:
envoy.kaasops.io/domains: "www.example.com,api.example.com,admin.example.com"
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # base64-encoded certificate
tls.key: LS0tLS1CRUdJTi... # base64-encoded private key
apiVersion: v1
kind: Secret
metadata:
name: wildcard-example-tls
namespace: production
annotations:
envoy.kaasops.io/domains: "*.example.com"
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # base64-encoded wildcard certificate
tls.key: LS0tLS1CRUdJTi... # base64-encoded private key
Cause: Auto discovery is enabled but no secret with matching domain annotation exists.
Solutions:
envoy.kaasops.io/domains annotationCause: Referenced secret doesn’t exist or is in a different namespace.
Solutions:
kubectl get secret <name> -n <namespace>secretRef.namespace field if referencing cross-namespaceCause: Multiple secrets exist for the same domain, and the selection algorithm picked a different one.
Solutions:
secretRef instead of autoDiscovery for precise controlCause: Certificate parsing failed (malformed PEM, corrupted data).
Solutions:
openssl x509 -in cert.pem -text -noouttls.crt key exists in secret dataCheck secret annotations:
kubectl get secret <name> -n <namespace> -o jsonpath='{.metadata.annotations}'
Verify certificate expiration:
kubectl get secret <name> -n <namespace> -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -dates
List all TLS secrets with domain annotations:
kubectl get secrets --all-namespaces -o json | \
jq -r '.items[] | select(.metadata.annotations["envoy.kaasops.io/domains"] != null) |
"\(.metadata.namespace)/\(.metadata.name): \(.metadata.annotations["envoy.kaasops.io/domains"])"'