Cài xong cert-manager trong 5 phút — bắt đầu luôn
Mình vừa chuyển một cụm Kubernetes sang dùng cert-manager tháng trước và thực sự không muốn quay lại cách quản lý chứng chỉ thủ công nữa. Trước đó mình dùng Certbot với cron job gia hạn. Cách đó ổn khi chỉ có vài domain — nhưng khi Ingress tăng lên vài chục thì bắt đầu loạn thật sự: sync cert giữa nhiều node, track ngày hết hạn từng domain, xử lý khi cron chạy không đúng giờ.
cert-manager giải quyết đúng vấn đề đó: nó chạy trong cluster, tự watch chứng chỉ sắp hết hạn (30 ngày trước), tự gia hạn, tự inject vào Kubernetes Secret. Không cần SSH vào server, không cần cron, không cần nhớ.
Bước 1: Cài cert-manager bằng Helm
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
Kiểm tra pods đã running chưa:
kubectl get pods -n cert-manager
Bạn sẽ thấy 3 pods: cert-manager, cert-manager-cainjector, và cert-manager-webhook. Tất cả phải ở trạng thái Running trước khi tiếp tục.
Bước 2: Tạo ClusterIssuer cho Let’s Encrypt
Có 2 môi trường: staging (để test, cert không được browser tin tưởng) và production. Mình khuyên test staging trước — bị rate limit Let’s Encrypt là phiền lắm, và hoàn toàn tránh được.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
kubectl apply -f clusterissuer.yaml
kubectl get clusterissuer
Bước 3: Annotate Ingress để tự động cấp cert
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-service
port:
number: 80
Apply xong, cert-manager sẽ tự tạo Certificate resource, thực hiện ACME challenge, và lưu cert vào Secret myapp-tls. Thường mất 1–2 phút. Theo dõi tiến trình:
kubectl describe certificate myapp-tls -n default
kubectl get certificaterequest -n default
Hiểu sâu hơn — cert-manager làm gì phía sau
Khi bạn annotate Ingress với cert-manager.io/cluster-issuer, cert-manager tạo ra một chuỗi resources theo thứ tự:
- Certificate — Định nghĩa cert cần tạo: domains, secret name, issuer
- CertificateRequest — Gửi CSR (Certificate Signing Request) lên Let’s Encrypt
- Order — Theo dõi ACME order với CA
- Challenge — Xử lý HTTP-01 hoặc DNS-01 challenge để prove domain ownership
Điểm mình thích nhất: cert-manager tự gia hạn khi cert còn 30 ngày — hoàn toàn configurable qua renewBefore. Trước đây mình từng để quên cert Certbot trên một server staging 2 tuần, không có ai nhắc, đến khi webhook của service khác gọi vào thì mới phát hiện cert hết hạn. Chuyện đó không xảy ra với cert-manager nữa.
HTTP-01 vs DNS-01 Challenge
HTTP-01 phù hợp cho đa số trường hợp: cert-manager tạo temporary endpoint trên domain của bạn để Let’s Encrypt verify. Yêu cầu domain phải trỏ vào cluster và port 80 phải mở từ internet.
DNS-01 dùng khi cần wildcard cert hoặc bảo mật internal services không expose ra ngoài: cert-manager tạo TXT record trên DNS provider. Cần tích hợp thêm với Cloudflare, Route53, hoặc các DNS provider khác.
Nâng cao: Wildcard cert và bảo mật internal services
Wildcard Certificate với DNS-01 (Cloudflare)
Setup này mình đang dùng cho production. Trước tiên tạo API token trên Cloudflare với quyền Zone:DNS:Edit cho zone cụ thể — không cấp Global API Key vì nó có quyền quá rộng, rủi ro không cần thiết.
Mình hay dùng password generator tại toolcraft.app khi cần tạo token ngẫu nhiên cho service account. Tool chạy hoàn toàn trên trình duyệt — không lo credential đi qua server bên thứ ba.
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=YOUR_CLOUDFLARE_TOKEN \
-n cert-manager
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-dns-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
Tạo wildcard cert dùng chung cho toàn bộ subdomain:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: default
spec:
secretName: wildcard-example-com-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- "example.com"
TLS cho internal services không có public IP
Use case này là chỗ cert-manager thực sự vượt trội. Prometheus, Grafana, ArgoCD thường chỉ accessible qua VPN hoặc bastion host — nhưng vẫn nên dùng HTTPS để tránh credential bị sniff trên mạng nội bộ.
DNS-01 chỉ cần verify qua DNS record, không cần HTTP access từ ngoài — nghĩa là internal services vẫn được cấp cert hợp lệ:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: grafana-internal-tls
namespace: monitoring
spec:
secretName: grafana-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- "grafana.internal.example.com"
Ingress của Grafana vẫn có HTTPS hợp lệ dù không expose port 80/443 ra internet. Credentials không bị sniff trên mạng nội bộ, và browser không hiện warning cert tự ký.
Tips thực tế từ kinh nghiệm vận hành
1. Luôn test với staging trước
Let’s Encrypt production có rate limit: 5 certificate failures/hour/domain, 50 certificates/domain/week. Bị hit rate limit là khóa domain cả tuần không issue được cert mới. Tạo ClusterIssuer staging riêng bằng cách thay server URL:
server: https://acme-staging-v02.api.letsencrypt.org/directory
2. Debug khi cert không được issue
90% lỗi mình gặp là do: (1) domain chưa trỏ đúng vào cluster, (2) ingress class name sai, (3) firewall block port 80 từ internet. Trace theo thứ tự resources này:
# Xem từng bước trong chuỗi
kubectl describe certificate <name> -n <namespace>
kubectl describe certificaterequest <name> -n <namespace>
kubectl describe order <name> -n <namespace>
kubectl describe challenge <name> -n <namespace>
# Xem logs cert-manager
kubectl logs -n cert-manager deploy/cert-manager -f
3. Monitor certificate expiry với Prometheus
cert-manager expose sẵn Prometheus metrics. Alert khi cert còn dưới 7 ngày — dù cert-manager tự gia hạn nhưng cứ alert cho chắc chắn:
certmanager_certificate_expiration_timestamp_seconds
certmanager_certificate_ready_status
4. Backup Kubernetes Secrets chứa cert
cert-manager lưu private key trong Kubernetes Secret. Kinh nghiệm xương máu: mình từng recreate namespace để dọn dẹp, Secret chứa cert bị xóa theo, mất khoảng 10 phút downtime để chờ cert mới được issue. Từ đó mình luôn dùng Velero backup Secrets định kỳ, hoặc ít nhất export Secret ra trước khi làm gì liên quan đến namespace:
kubectl get secret myapp-tls -n default -o yaml > myapp-tls-backup.yaml
5. Tái sử dụng wildcard cert cho nhiều Ingress
Khi đã có wildcard cert, reference cùng Secret trong nhiều Ingress thay vì tạo cert riêng cho từng subdomain — giảm số lần gọi Let’s Encrypt API và dễ quản lý hơn:
spec:
tls:
- hosts:
- api.example.com
secretName: wildcard-example-com-tls # Dùng lại Secret từ Certificate đã tạo
Mình đang chạy cert-manager trên cluster với khoảng 30 Ingress và chưa một lần phải can thiệp thủ công vào chứng chỉ nào. Số Ingress tăng thêm bao nhiêu cũng không phát sinh việc quản lý. Nếu bạn đang dùng Kubernetes mà vẫn còn cron job gia hạn cert đâu đó — đây là lúc cần thay đổi.

