Tóm tắt: Cài ingress-nginx và cert-manager bằng Helm, tạo ClusterIssuer ACME (staging trước, production sau), rồi gắn annotation cert-manager.io/cluster-issuer vào Ingress. cert-manager tự tạo Certificate, giải challenge HTTP-01 qua chính ingress-nginx, lưu cert vào Secret ở spec.tls[].secretName và gia hạn trước khi hết hạn.
Dự án ingress-nginx (kubernetes/ingress-nginx) đã ngừng phát triển: Kubernetes SIG Network thông báo chỉ bảo trì best-effort tới tháng 3/2026, sau đó không còn bản phát hành hay bản vá bảo mật. Cluster mới nên cân nhắc controller khác hoặc Gateway API (cert-manager cũng hỗ trợ Gateway). Phần cert-manager dưới đây áp dụng được cho các controller khác, chỉ cần đổi ingressClassName.
Luồng hoạt động
flowchart LR I["Ingress + annotation cluster-issuer"] --> C[Certificate] C --> O["Order / Challenge"] O -->|"HTTP-01 /.well-known/acme-challenge"| LE["Let's Encrypt"] LE -->|"GET qua ingress-nginx"| S["solver Pod tạm"] LE -->|"cấp cert"| K["Secret TLS"] K --> N["ingress-nginx dùng cert"]
Cấu hình
Cài ingress-nginx và cert-manager
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
-n ingress-nginx --create-namespace
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm upgrade --install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace \
--set crds.enabled=true
kubectl get svc -n ingress-nginx ingress-nginx-controller # lấy EXTERNAL-IP để trỏ DNS
kubectl get pods -n cert-managercrds.enabled=true là tên value từ cert-manager 1.15 (bản cũ dùng installCRDs=true). Trỏ bản ghi A/AAAA của app.example.com về EXTERNAL-IP của controller trước khi xin cert.
ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginxkubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer # cột READY phải TrueClusterIssuer dùng cho mọi namespace; Issuer chỉ trong một namespace (annotation tương ứng là cert-manager.io/issuer).
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
ingressClassName: nginx
tls:
- hosts: [app.example.com]
secretName: app-example-com-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80Chạy với staging tới khi Certificate READY, rồi đổi annotation sang letsencrypt-prod và xoá Secret staging để cert-manager cấp lại:
kubectl annotate ingress app -n web cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite
kubectl delete secret app-example-com-tls -n webWildcard bằng DNS-01 (Cloudflare)
HTTP-01 không cấp được *.example.com. Dùng DNS-01, token API để trong Secret ở namespace cert-manager (namespace của cluster resource):
kubectl create secret generic cloudflare-api-token -n cert-manager --from-literal=api-token="$CF_API_TOKEN" solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenLệnh kiểm tra
| Mục đích | Lệnh |
|---|---|
| Certificate đã READY chưa | kubectl get certificate -n web |
| Lý do chưa cấp | kubectl describe certificate app-example-com-tls -n web |
| Đi xuống từng tầng | kubectl get certificaterequest,order,challenge -n web |
| Chi tiết challenge đang kẹt | kubectl describe challenge -n web |
| Log cert-manager | kubectl logs -n cert-manager deploy/cert-manager --tail=100 |
| Cert thực tế trong Secret | kubectl get secret app-example-com-tls -n web -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -issuer -dates |
| Thử path challenge từ ngoài | curl -sv http://app.example.com/.well-known/acme-challenge/test |
| Log controller ingress-nginx | kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=100 |
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
Không có Certificate nào được tạo |
Sai tên annotation, hoặc Ingress thiếu spec.tls |
Annotation đúng là cert-manager.io/cluster-issuer, phải có tls.secretName |
ClusterIssuer READY False |
Không đăng ký được account ACME (sai URL server, chặn egress) | kubectl describe clusterissuer letsencrypt-prod |
Challenge pending, lỗi wrong status code '404' |
DNS chưa trỏ về ingress, hoặc solver dùng sai ingress class | Kiểm tra DNS, ingressClassName trong solver khớp controller |
Challenge lỗi connection refused/timeout |
Port 80 bị firewall/LB chặn | Mở port 80 từ internet (HTTP-01 bắt buộc dùng 80) |
Trình duyệt báo cert (STAGING) không tin cậy |
Đang dùng issuer staging | Đổi sang letsencrypt-prod, xoá Secret cũ |
Lỗi too many certificates already issued / rateLimited |
Vượt rate limit Let's Encrypt production | Thử trên staging, đợi hết thời gian giới hạn |
Truy cập ra cert Kubernetes Ingress Controller Fake Certificate |
Secret chưa có hoặc tên không khớp secretName |
Chờ Certificate READY, kiểm tra tên Secret và namespace |
| Redirect loop sau Cloudflare proxy | ingress-nginx ép HTTPS trong khi Cloudflare gửi HTTP tới origin | Đặt SSL mode Full (strict) trên Cloudflare |
Kiến thức chain/SAN khi đọc cert: TLS troubleshooting. Lệnh Helm: Helm – lệnh hay dùng.