Triển khai Envoy Gateway + cert-manager + Cloudflare DNS-01 (wildcard) trên Kubernetes
May 28, 2026

End-to-end guide: từ cluster Kubernetes trắng đến một Gateway HTTPS có wildcard cert Let's Encrypt, tự renew, kèm default landing page cho subdomain chưa cấu hình.
1. Mục tiêu
Bài này dựng một edge layer cho Kubernetes có 4 tính chất:
- TLS wildcard
*.example.com— không phải cấp cert cho từng subdomain. - Auto-renew — cert hết hạn cứ 90 ngày, cert-manager tự xoay.
- HTTP→HTTPS redirect mặc định.
- Default backend — subdomain chưa cấu hình route thì rơi về một trang landing thay vì 404.
Stack:
| Thành phần | Version | Vai trò |
|---|---|---|
| Kubernetes | 1.30.x | Cluster (bài này dùng VKS của VNG, nhưng bất kỳ k8s nào đều dùng được) |
| Gateway API | v1.2.1 (standard) | CRDs Gateway, HTTPRoute, … |
| Envoy Gateway | v1.3.0 | Implementation của Gateway API (data plane là Envoy) |
| cert-manager | v1.20.x | Xin/renew cert qua ACME |
| Let's Encrypt | — | CA |
| Cloudflare | — | DNS provider cho DNS-01 challenge |
Domain mẫu trong bài: *.sb.annd.io.vn. Hãy thay bằng domain của bạn khi đọc.
Các version trên là tại thời điểm viết bài. Khi đọc, hãy thay bằng phiên bản mới nhất tương thích — Gateway API và Envoy Gateway minor-bump khá nhanh, còn cert-manager thì nhớ pin
>= 1.17nếu dùng Cloudflare DNS-01 (xem mục 9.2).
2. Kiến thức nền tảng
Trước khi đi vào triển khai, cùng định nghĩa nhanh các khái niệm chính.
Gateway API: Là project kế nhiệm chính thức của Ingress trong Kubernetes, do SIG-Network phát triển. Tách rõ vai trò
GatewayClass(infra-ops),Gateway(cluster-ops, mở listener/cấp cert) vàHTTPRoute(app-ops, gắn route vào Gateway). Hỗ trợ đa giao thức HTTP/gRPC/TCP/TLS và các tính năng nâng cao như traffic split, header rewrite, redirect được chuẩn hoá ở mức spec.
Envoy Gateway: Là implementation tham chiếu của Gateway API được CNCF endorse, dùng Envoy làm data plane. Nhẹ hơn full service mesh (Istio) nhưng vẫn hưởng performance và feature của Envoy.
cert-manager: Là Kubernetes add-on tự động hoá việc xin và gia hạn TLS certificate. Hỗ trợ nhiều issuer (Let's Encrypt, HashiCorp Vault, Venafi, CA tự ký…) và nhiều giải pháp xác minh sở hữu domain.
ACME (Automatic Certificate Management Environment): Là giao thức cho phép tự động hoá việc xác minh domain và cấp phát certificate. Let's Encrypt là CA dùng ACME phổ biến nhất hiện nay.
DNS-01 challenge: Là phương thức ACME chứng minh sở hữu domain bằng cách yêu cầu chủ domain tạo một TXT record dạng
_acme-challenge.example.comvới giá trị do CA quy định. Khác với HTTP-01, DNS-01 không cần expose port 80 và là cách duy nhất để xin wildcard cert.
Wildcard certificate: Là TLS certificate cover tất cả subdomain cấp 1 của một domain, ví dụ
*.example.comcoverapp.example.com,api.example.com… nhưng không coverapp.foo.example.com.
Tại sao bộ ba này, mà không phải Ingress + cert-manager?
Ingress đang dần lỗi thời. Gateway API có model rõ hơn (tách Gateway cluster-ops vs HTTPRoute app-ops), spec đa giao thức (HTTP/gRPC/TCP/TLS), và biểu hiện chuẩn cho các tính năng nâng cao (traffic split, header rewrite, redirect…).
Envoy Gateway là implementation tham chiếu được CNCF endorse, dùng Envoy làm data plane. Nó nhẹ hơn full Istio nhưng vẫn hưởng performance/feature của Envoy.
Wildcard cert bắt buộc DNS-01. Let's Encrypt không cho *.example.com qua HTTP-01 vì không xác định được phải gọi subdomain nào để verify. DNS-01 chứng minh sở hữu domain bằng cách tạo TXT record _acme-challenge.example.com. Vì vậy cert-manager cần API quyền tạo/xoá DNS record — ở đây dùng Cloudflare API token.
3. Kiến trúc
Client --HTTPS--> LoadBalancer --> Envoy Proxy --> HTTPRoute --> Service --> Pod
|
| TLS terminate
v
Secret wildcard-tls
^
| create / renew
cert-manager --DNS-01--> Cloudflare API
^
| ACME
Let's Encrypt
Phân tách namespace:
| Namespace | Chứa |
|---|---|
envoy-gateway-system | Controller Envoy Gateway + data-plane Envoy pods |
cert-manager | cert-manager controller + Cloudflare API token Secret |
envoy-gateway | Gateway, wildcard Certificate, HTTPRoute hệ thống |
<app> | App của bạn + HTTPRoute riêng |
4. Chuẩn bị
4.1. Cluster + tool
kubectl version --client # >= 1.27 ok
helm version # v3.x
kubectl cluster-info # phải kết nối được tới cluster
Cluster cần có một mechanism cấp LoadBalancer External IP — managed k8s thường có sẵn (cloud LB). Nếu chạy on-prem, cài MetalLB trước.
4.2. Cloudflare API token
Trên Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token:
| Permission | Resource |
|---|---|
| Zone — Zone — Read | Include — Specific zone — example.com |
| Zone — DNS — Edit | Include — Specific zone — example.com |
Lưu ý: dùng API Token, không dùng Global API Key. Nếu wildcard là *.sub.example.com và Cloudflare zone là example.com, scope token vào example.com (zone gốc), không phải sub.example.com.
5. Cài đặt từng bước
5.1. Gateway API CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
Verify:
kubectl get crd | grep gateway.networking.k8s.io
# gateways, httproutes, gatewayclasses, grpcroutes, referencegrants
5.2. cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.20.2 \
--set crds.enabled=true
Đừng cài v1.16.x. Có bug khi cleanup DNS record qua Cloudflare API (sẽ phân tích ở phần Troubleshooting). Dùng v1.17+ hoặc mới hơn.
Verify:
kubectl -n cert-manager get pods
# cert-manager, cert-manager-cainjector, cert-manager-webhook đều Running
5.3. Envoy Gateway
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.3.0 \
--namespace envoy-gateway-system --create-namespace
Verify:
kubectl -n envoy-gateway-system get pods
# envoy-gateway-xxx Running
5.4. Cloudflare API token Secret
Không commit token vào git. Tạo Secret trực tiếp bằng kubectl:
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token='<paste-token-here>'
5.5. ClusterIssuer (staging + prod)
Tạo clusterissuer-staging.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: you@example.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- example.com
Và clusterissuer-prod.yaml — y hệt nhưng đổi server thành https://acme-v02.api.letsencrypt.org/directory, đổi name thành letsencrypt-prod, privateKeySecretRef.name thành letsencrypt-prod-account-key.
Vì sao có cả staging và prod? Let's Encrypt prod có rate limit: 50 certificates per Registered Domain / tuần (limit chính), và 5 Duplicate Certificates / tuần khi cấp đúng cùng tập SAN. Mỗi sai sót (typo token, sai zone, DNS chưa propagate) sẽ ăn vào hạn mức. Staging không rate limit, dùng staging cho đến khi pipeline issue thành công ổn định, rồi mới đổi sang prod.
Apply:
kubectl apply -f clusterissuer-staging.yaml -f clusterissuer-prod.yaml
kubectl get clusterissuer
# letsencrypt-staging True
# letsencrypt-prod True
5.6. GatewayClass + Gateway
gatewayclass.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
gateway.yaml (chú ý secretName ở dưới phải khớp với Certificate.spec.secretName mục 5.7):
apiVersion: v1
kind: Namespace
metadata:
name: envoy-gateway
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: eg
namespace: envoy-gateway
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "*.example.com"
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-example-com-tls
allowedRoutes:
namespaces:
from: All
HTTP→HTTPS redirect — thay vì redirect ở từng app, đặt 1 catch-all HTTPRoute trên listener http:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-to-https-redirect
namespace: envoy-gateway
spec:
parentRefs:
- name: eg
sectionName: http
hostnames:
- "*.example.com"
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Apply:
kubectl apply -f gatewayclass.yaml -f gateway.yaml
Lúc này HTTPS listener sẽ chưa programmed vì Secret cert chưa tồn tại — đó là kỳ vọng. Listener HTTP và redirect đã chạy.
kubectl -n envoy-gateway describe gateway eg
# https listener: "Secret envoy-gateway/wildcard-example-com-tls does not exist." — OK
5.7. Certificate (wildcard)
certificate.yaml:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: envoy-gateway
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-staging # đổi sang letsencrypt-prod sau khi verify
kind: ClusterIssuer
commonName: "*.example.com"
dnsNames:
- "example.com"
- "*.example.com"
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always
duration: 2160h # 90d
renewBefore: 360h # auto-renew ~15d trước hạn
Apply rồi theo dõi:
kubectl apply -f certificate.yaml
# Theo dõi tiến trình (1-3 phút)
kubectl -n envoy-gateway get certificate,certificaterequest,order,challenge
# Khi Ready=True, Secret xuất hiện
kubectl -n envoy-gateway get secret wildcard-example-com-tls
Flow nội bộ: Certificate → cert-manager tạo CertificateRequest → tạo Order ở ACME → tạo các Challenge (1 per dnsName) → present TXT lên Cloudflare → LE verify → cert issued → write Secret.
Certificate --> CertificateRequest --> Order --> Challenge
|
| present TXT record
v
Cloudflare DNS
^
| verify
|
Let's Encrypt (ACME)
5.8. Chuyển sang Let's Encrypt prod
Sau khi staging chạy ổn:
# Sửa issuerRef.name: letsencrypt-staging -> letsencrypt-prod
kubectl apply -f certificate.yaml
# Xoá secret cũ để force re-issue từ prod
kubectl -n envoy-gateway delete secret wildcard-example-com-tls
cert-manager sẽ tự re-issue, vài chục giây sau verify bằng:
kubectl -n envoy-gateway get secret wildcard-example-com-tls -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -issuer -dates
# issuer=C=US, O=Let's Encrypt, CN=... (intermediate có thể là E5, E7, R10, R11... tuỳ thời điểm; quan trọng là KHÔNG còn "STAGING")
5.9. DNS record
Lấy External IP/hostname của LB:
kubectl -n envoy-gateway-system get svc \
-l gateway.envoyproxy.io/owning-gateway-name=eg
Tạo trên Cloudflare:
A *.example.com <EXTERNAL-IP> (DNS only - KHONG bat proxy)
A example.com <EXTERNAL-IP> (neu can root)
Tại sao tắt Cloudflare proxy (orange cloud)? Khi proxy bật, Cloudflare làm TLS termination với cert của Cloudflare; cert LE wildcard chỉ chạy ở chặng origin. Trường hợp đó vẫn dùng được nhưng kiến trúc thay đổi (full strict mode, origin cert riêng…). Mặc định cứ DNS only cho đến khi cần CDN/WAF.
6. Deploy app đầu tiên
Mẫu (whoami):
apiVersion: v1
kind: Namespace
metadata: { name: demo }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: whoami, namespace: demo }
spec:
replicas: 1
selector: { matchLabels: { app: whoami } }
template:
metadata: { labels: { app: whoami } }
spec:
containers:
- name: whoami
image: traefik/whoami:v1.10
ports: [{ containerPort: 80 }]
---
apiVersion: v1
kind: Service
metadata: { name: whoami, namespace: demo }
spec:
selector: { app: whoami }
ports: [{ port: 80, targetPort: 80 }]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: whoami, namespace: demo }
spec:
parentRefs:
- name: eg
namespace: envoy-gateway
sectionName: https
hostnames: ["whoami.example.com"]
rules:
- matches: [{ path: { type: PathPrefix, value: / } }]
backendRefs:
- { name: whoami, port: 80 }
Tạo CNAME/A whoami.example.com → <EXTERNAL-IP>, rồi:
curl https://whoami.example.com/
# Hostname: whoami-xxx ...
Vì HTTPRoute nằm khác namespace với Gateway, cần parentRefs.namespace chỉ rõ — Gateway.spec.listeners[*].allowedRoutes.namespaces.from: All đã cho phép.
7. Default landing page cho subdomain chưa cấu hình
Pattern: 1 HTTPRoute với hostname wildcard *.example.com. Gateway API spec đảm bảo hostname cụ thể thắng wildcard — không cần config priority.
Request: foo.example.com
|
v
Match HTTPRoute by hostname
/ \
specific match / \ no specific match
v v
whoami.example.com *.example.com
(whoami app) (landing page)
apiVersion: v1
kind: Namespace
metadata: { name: landing }
---
apiVersion: v1
kind: ConfigMap
metadata: { name: landing-html, namespace: landing }
data:
index.html: |
<!doctype html><html><body>
<h1>example.com</h1>
<p>Subdomain chưa được cấu hình route.</p>
</body></html>
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: landing, namespace: landing }
spec:
replicas: 1
selector: { matchLabels: { app: landing } }
template:
metadata: { labels: { app: landing } }
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports: [{ containerPort: 80 }]
volumeMounts:
- { name: html, mountPath: /usr/share/nginx/html }
volumes:
- name: html
configMap: { name: landing-html }
---
apiVersion: v1
kind: Service
metadata: { name: landing, namespace: landing }
spec:
selector: { app: landing }
ports: [{ port: 80, targetPort: 80 }]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: landing, namespace: landing }
spec:
parentRefs:
- { name: eg, namespace: envoy-gateway, sectionName: https }
hostnames: ["*.example.com"]
rules:
- matches: [{ path: { type: PathPrefix, value: / } }]
backendRefs:
- { name: landing, port: 80 }
Test:
curl https://whoami.example.com/ # -> whoami (specific match thang)
curl https://foo.example.com/ # -> landing
curl https://bar.example.com/ # -> landing
8. Verify end-to-end
# Cert
kubectl -n envoy-gateway get secret wildcard-example-com-tls -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# Gateway
kubectl -n envoy-gateway get gateway eg -o jsonpath='{.status.conditions}' | jq
# HTTPS
curl -v https://whoami.example.com/
# Redirect
curl -sI http://whoami.example.com/
# HTTP/1.1 301 Moved Permanently
# location: https://whoami.example.com/
9. Troubleshooting
9.1. Cert kẹt ở Ready=False
kubectl -n envoy-gateway describe certificate wildcard-example-com
kubectl -n envoy-gateway describe order $(kubectl -n envoy-gateway get order -o name | head -1)
kubectl -n envoy-gateway describe challenge $(kubectl -n envoy-gateway get challenge -o name | head -1)
kubectl -n cert-manager logs deploy/cert-manager --tail=200
9.2. Cloudflare API error 7003: Could not route to /zones//dns_records/<id>
URL có // (zone_id rỗng). Trong môi trường thực tế đã gặp với cert-manager v1.16.2: CREATE TXT record thành công, nhưng cleanup DELETE truyền zone_id="" → order kẹt sau khi 1 challenge valid.
Fix: upgrade lên v1.17+ (đang ổn ở v1.20.x).
helm upgrade cert-manager jetstack/cert-manager \
-n cert-manager --version v1.20.2 \
--set crds.enabled=true
kubectl -n envoy-gateway delete certificaterequest --all
kubectl -n envoy-gateway delete order --all
kubectl -n envoy-gateway delete challenge --all
# cert-manager tự tạo lại từ Certificate
9.3. Challenge ở trạng thái pending quá lâu
-
Token Cloudflare thiếu
Zone:Readcho zone tương ứng → cert-manager không tìm được zone_id. -
dnsZonesselector trong ClusterIssuer chỉ tới zone con không tồn tại — phải là zone gốc trong Cloudflare (example.com, không phảisub.example.comtrừ khisub.example.comthực sự là 1 zone độc lập đã delegate NS). -
DNS chưa propagate — DNS-01 phải chờ vài chục giây đến vài phút. Test thủ công:
bashdig TXT _acme-challenge.example.com @1.1.1.1
9.4. HTTPS listener Programmed=False với InvalidCertificateRef
Secret envoy-gateway/wildcard-example-com-tls does not exist.
Tên Certificate.spec.secretName không khớp Gateway.listeners[https].tls.certificateRefs[0].name. Hoặc cert chưa issue. Hoặc nằm khác namespace với Gateway (cần ReferenceGrant).
9.5. App trả 404 từ Envoy
kubectl -n <app-ns> describe httproute <name>
Check parentRefs đúng tên/namespace/sectionName, hostnames khớp với host trong request, listener tương ứng có allowedRoutes.namespaces.from: All (hoặc dùng Selector với label trên namespace).
10. Operations
10.1. Renew
cert-manager kiểm tra renewBefore định kỳ. Với duration=2160h, renewBefore=360h, cert sẽ renew khi còn ~15 ngày — không cần làm gì.
Force renew thủ công:
# cmctl là binary độc lập của cert-manager (https://cert-manager.io/docs/reference/cmctl/)
cmctl renew wildcard-example-com -n envoy-gateway
# Hoặc nếu cài làm kubectl plugin:
# kubectl cm renew wildcard-example-com -n envoy-gateway
10.2. Thêm subdomain mới
Chỉ cần:
- Tạo Service + HTTPRoute mới với
hostnames: [newapp.example.com]. - Tạo DNS record CNAME/A trên Cloudflare.
Không cần cấp cert mới — wildcard cover tất cả *.example.com.
10.3. Rotate Cloudflare token
kubectl -n cert-manager create secret generic cloudflare-api-token \
--from-literal=api-token='<new-token>' \
--dry-run=client -o yaml | kubectl apply -f -
cert-manager pick up Secret mới ở lần ACME tiếp theo. Không cần restart.
10.4. Backup gì?
- Account key của ACME (
letsencrypt-prod-account-keySecret trongcert-managerns) — mất key thì phải đăng ký account mới, nhưng không mất quyền issue cert vì cert vẫn ở Secret hiện tại. Tốt nhất là backup. - Cloudflare token — quản lý ngoài cluster.
- Cert Secret — không cần backup; cert-manager tự renew từ ACME.
11. Lấy real client IP với PROXY protocol
Mặc định cloud LoadBalancer là L4 — packet đến Envoy thì source IP là IP của LB node, không phải IP thật của client. Nếu app cần biết IP thật (rate limit, audit log, geo-IP, anti-fraud), phải bật PROXY protocol.
PROXY protocol: Là giao thức do HAProxy đề xuất (v1 text, v2 binary), cho phép L4 proxy/LB chèn 1 header ở đầu TCP stream chứa source IP/port + destination IP/port của connection gốc. Backend đọc header này để biết IP thật của client. Khác với HTTP
X-Forwarded-For(chỉ áp được với HTTP), PROXY protocol làm việc ở tầng TCP nên hoạt động cả với HTTPS pass-through, TLS termination, gRPC, raw TCP...
Bật PROXY protocol cần làm ở cả 2 chặng:
- LB → Envoy: LB chèn PROXY header. Đây là setting trên cloud LB, kích hoạt qua annotation trên Service
type: LoadBalancer. - Envoy decode header: Envoy listener phải biết expect PROXY header để parse đúng. Trong Envoy Gateway, dùng
ClientTrafficPolicy.
Nếu chỉ bật 1 trong 2 chặng, kết nối sẽ hỏng: bật ở LB mà Envoy không expect → Envoy thấy junk bytes; bật ở Envoy mà LB không gửi → Envoy timeout chờ header. Vì vậy, apply 2 manifest gần như đồng thời.
Client (203.0.113.42)
|
v
Cloud LB ----[ PROXY v2 hdr: 203.0.113.42:54321 ]----> Envoy listener
|
v
Pod sees X-Forwarded-For: 203.0.113.42
11.1. Bật PROXY protocol ở LB (Service annotation)
Envoy Gateway tự tạo Service type: LoadBalancer cho Gateway. Để inject annotation lên Service đó, dùng CR EnvoyProxy rồi attach vào GatewayClass.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: proxy-config
namespace: envoy-gateway-system
spec:
provider:
type: Kubernetes
kubernetes:
envoyService:
annotations:
# VKS (VNG Cloud) - bat proxy protocol cho moi port
vks.vngcloud.vn/enable-proxy-protocol: "*"
# AWS NLB:
# service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
# GCP (regional ext LB):
# cloud.google.com/l4-rbs: "enabled"
envoyDeployment:
replicas: 2
Gắn EnvoyProxy vào GatewayClass qua parametersRef:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: proxy-config
namespace: envoy-gateway-system
Apply, đợi Envoy Gateway reconcile rồi check Service đã có annotation và cloud LB đã enable PROXY protocol:
kubectl -n envoy-gateway-system get svc \
-l gateway.envoyproxy.io/owning-gateway-name=eg -o yaml | grep -A2 annotations
11.2. Bật PROXY protocol ở Envoy listener (ClientTrafficPolicy)
ClientTrafficPolicy cho phép tinh chỉnh behaviour ở phía listener — target vào toàn bộ Gateway hoặc 1 listener cụ thể.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: enable-proxy-protocol
namespace: envoy-gateway
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: eg
enableProxyProtocol: true
Apply rồi verify status:
kubectl -n envoy-gateway describe clienttrafficpolicy enable-proxy-protocol
# Conditions:
# Type: Accepted
# Status: True
11.3. Verify real IP đã pass-through
Deploy whoami (đã có ở mục 6) — nó echo headers, rất tiện để debug. Sau khi bật 2 chặng trên:
curl https://whoami.example.com/
# ...
# X-Forwarded-For: 203.0.113.42
# X-Envoy-External-Address: 203.0.113.42
# X-Real-IP: 203.0.113.42
Envoy tự synthesize X-Forwarded-For, X-Envoy-External-Address từ PROXY header. App downstream chỉ cần đọc 1 trong các header này.
Nếu chưa thấy IP thật:
- Annotation chưa propagate xuống cloud LB — check raw Service:
kubectl -n envoy-gateway-system get svc <eg-svc> -o yaml. - ClientTrafficPolicy
Accepted=False— describe sẽ thấy lỗi (sai targetRef, conflict policy…). - Cloud LB không hỗ trợ PROXY v2 — check doc của provider. Cloudflare proxy mode không truyền PROXY protocol mặc định, phải dùng Spectrum hoặc bật origin-side khác.
- TCP connection bị reset ngay khi bật → 1 trong 2 chặng còn chưa đồng bộ (LB gửi header mà listener không expect, hoặc ngược lại). Rollback policy ngay rồi apply lại đồng thời.
11.4. Lưu ý vận hành
- Apply 2 chặng atomic. Trong production, apply 2 manifest trong 1 lệnh
kubectl apply -f envoyproxy.yaml -f clienttrafficpolicy.yamlđể giảm thời gian connection hỏng. - Trust hops. Khi có thêm CDN trước cloud LB (Cloudflare proxy, AWS CloudFront),
X-Forwarded-Forsẽ là 1 chuỗiclient, cdn, lb. Cấu hìnhnumTrustedHopstrongClientTrafficPolicy.spec.clientIPDetection.xForwardedForđể Envoy biết chọn IP nào làm "client thật". - Overhead ~28 bytes/connection (PROXY v2 binary) — không đáng kể, nhưng tăng nhẹ với connection ngắn.
12. Tóm tắt
- Wildcard cert ⇒ DNS-01 ⇒ cần API token DNS provider. Token scope hẹp (chỉ DNS:Edit + Zone:Read trên đúng zone).
- Staging trước, prod sau — đừng đốt rate limit của LE.
- Gateway API: hostname specific thắng wildcard — pattern landing page rất gọn.
- Envoy Gateway nhẹ, đủ cho phần lớn use case mà không cần full service mesh.
- Bám sát version cert-manager >= 1.17 nếu dùng Cloudflare DNS-01.
- Muốn real client IP: bật PROXY protocol ở cả LB và Envoy listener (không bao giờ chỉ 1 bên).
Toàn bộ manifests và script kiểm chứng có thể nhóm theo thư mục manifests/ đánh số theo thứ tự apply, rất tiện cho việc setup lại từ đầu hoặc dựng môi trường mới chỉ bằng kubectl apply -k.
References
- Gateway API — Kubernetes SIG-Network
- Envoy Gateway — Official site
- Envoy Gateway — Quickstart guide
- cert-manager — Documentation
- cert-manager — Cloudflare DNS-01 solver
- Let's Encrypt — Challenge types
- Let's Encrypt — Rate limits
- ACME RFC 8555
- Cloudflare — Create API token
- Gateway API — HTTPRoute hostname matching spec
- PROXY protocol — Specification (HAProxy)
- Envoy Gateway — ClientTrafficPolicy (PROXY protocol)
- Envoy Gateway — Customize Envoy Proxy (EnvoyProxy CR)
Related posts
- Cấu hình TLS Certificate với Nginx Ingress, Cert-Manager và Let's Encrypt — Cùng cert-manager nhưng với HTTP-01 và Nginx Ingress, dùng cho single-domain.
- Configure TLS Certificate with HAProxy Ingress and Cert-Manager — Lựa chọn HAProxy Ingress thay cho Gateway API.
- Kubernetes CNI Overview — Hiểu phần mạng phía dưới Gateway/Service.
- MetalLB Load Balancer trên Kubernetes — Khi cluster on-prem cần External IP cho LoadBalancer service.
