Cert-Manager trên Kubernetes: Tự động cấp phát và gia hạn TLS từ Let’s Encrypt — Không còn lo chứng chỉ hết hạn

Security tutorial - IT technology blog
Security tutorial - IT technology blog

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.

Share: