How‑To: cert‑manager + Let’s Encrypt (DNS‑01 via Cloudflare) with Envoy TLS Termination
Overview
Issue real TLS certs with cert‑manager using the DNS‑01 challenge (Cloudflare) and terminate TLS directly in Envoy. No Ingress controller required.
Why DNS‑01 (no Ingress):
- Works with any L7 proxy (Envoy) without exposing port 80
- No /.well-known routing hassles
- Great for home/edge networks where HTTP‑01 is flaky
Prerequisites
- Kubernetes cluster with Envoy running as a Deployment/DaemonSet
- A domain in Cloudflare and an API Token with Zone.DNS:Edit for the zone
- cert‑manager installed (CRDs installed)
Install cert‑manager (Helm):
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager --namespace cert-manager --set installCRDs=true
Create Cloudflare API token Secret (in cert‑manager ns):
kubectl -n cert-manager create secret generic cloudflare-api-token-secret --from-literal=api-token='<CF_API_TOKEN>'
Create a ClusterIssuer for Let’s Encrypt (staging + prod) using DNS‑01:
cat <<'YAML' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-dns01
spec:
acme:
email: you@example.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: le-staging-dns01-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns01
spec:
acme:
email: you@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: le-prod-dns01-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
YAML
Request a Certificate (in your app namespace):
cat <<'YAML' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-cert
namespace: apps
spec:
secretName: envoy-tls-app # cert-manager will keep this secret updated
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod-dns01
dnsNames:
- app.example.com
YAML
Mount the TLS Secret into Envoy and configure TLS:
# Deployment snippet (Envoy)
spec:
template:
spec:
volumes:
- name: tls
secret:
secretName: envoy-tls-app
containers:
- name: envoy
image: envoyproxy/envoy:v1.30.2
volumeMounts:
- name: tls
mountPath: /etc/envoy/tls
readOnly: true
args: ["-c","/etc/envoy/envoy.yaml"]
Envoy listener (TLS) snippet (envoy.yaml):
static_resources:
listeners:
- name: https_listener
address:
socket_address: { address: 0.0.0.0, port_value: 443 }
filter_chains:
- filter_chain_match:
server_names: ["app.example.com"]
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "/etc/envoy/tls/tls.crt" }
private_key: { filename: "/etc/envoy/tls/tls.key" }
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: https_app
route_config:
name: local_route
virtual_hosts:
- name: app
domains: ["app.example.com"]
routes:
- match: { prefix: "/" }
route: { cluster: app_cluster }
http_filters:
- name: envoy.filters.http.router
clusters:
- name: app_cluster
connect_timeout: 0.3s
type: STRICT_DNS
load_assignment:
cluster_name: app_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: myapp.apps.svc.cluster.local
port_value: 8080
Renewal Strategy (choose one)
A) **Rolling restart on Secret change (simple):** watch the Secret and `kubectl rollout restart deploy/envoy` when it updates. Zero‑config but brief connection churn.
B) **SDS (advanced, zero‑downtime):** serve TLS secrets to Envoy via xDS/Secret Discovery so certs rotate without restarts.
Verify
kubectl -n apps get certificate,secret | grep app-cert
openssl s_client -connect app.example.com:443 -servername app.example.com -showcerts </dev/null | openssl x509 -noout -issuer -enddate
Security
- Scope the Cloudflare token to the specific zone only.
- Prefer DNS‑01 for private/edge sites; keep 80/443 locked down appropriately.
- Treat TLS secrets as sensitive; restrict RBAC.
Meme idea
“HTTP‑01: find me on port 80.” Me with Envoy only: “Bestie, I’m doing DNS.”
Taylor Swift
“And the monsters turned out to be just trees.” — Out of the Woods
Comments
Post a Comment