Bài toán: vòng đời chứng chỉ TLS

Cert TLS có hạn sử dụng (Let’s Encrypt: 90 ngày). Quản lý thủ công:

[1] Cấp / mua cert
│
▼
[2] Cài lên server
│
▼
[3] Đặt nhắc gia hạn (~60 ngày trước khi hết hạn)
│
▼
[4] Gia hạn thủ công

cert-manager tự động hóa toàn bộ vòng này trong Kubernetes:

[1] cert-manager watch Certificate
│
▼
[2] Cert sắp hết hạn (<~30 ngày) → auto-renew
│
▼
[3] ACME challenge (DNS-01 / HTTP-01)
│
▼
[4] Lưu cert vào Kubernetes Secret
│
▼
[5] Ingress / workload mount TLS Secret

ACME và hai loại challenge

Let’s Encrypt dùng giao thức ACME để xác minh bạn sở hữu domain:

HTTP-01 challenge

[1] Let's Encrypt bảo cert-manager: phục vụ `/.well-known/acme-challenge/TOKEN`
│
▼
[2] cert-manager tạo rule Ingress/Pod tạm để serve file
│
▼
[3] LE GET công khai vào domain → thấy file → verify → cấp cert

Yêu cầu: domain phải trỏ vào cluster và cổng 80 phải mở công khai. Không dùng được với wildcard cert.

DNS-01 challenge

[1] Let's Encrypt bảo cert-manager: tạo TXT `_acme-challenge.example.com = TOKEN`
│
▼
[2] cert-manager gọi API DNS provider (Route53, Cloudflare…) tạo bản ghi
│
▼
[3] LE query DNS → thấy TXT → verify → cấp cert

Yêu cầu: cert-manager cần credential API của DNS provider. Dùng được với wildcard cert và với domain không expose HTTP.


Cài cert-manager

# Cài bằng Helm
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true

# Kiểm tra
kubectl get pods -n cert-manager

Tạo ClusterIssuer (Let’s Encrypt)

# HTTP-01 challenge
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

class vs ingressClassName: field class cũ (dùng annotation kubernetes.io/ingress.class) đã deprecated từ K8s 1.18; cert-manager v1.12+ đọc ingressClassName khớp với IngressClass resource chính thức. Nếu thấy challenge Pod không được route đúng, nguyên nhân thường là dùng class sai tên hoặc Ingress mới khai báo spec.ingressClassName khác.

Lưu ý: Dùng letsencrypt-staging khi test để tránh rate limit của production. Staging cert không được browser tin nhưng không có rate limit.


Certificate resource và tích hợp với Ingress

Cách 1: Annotation trên Ingress (đơn giản hơn)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-api
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-example-com-tls # cert-manager tự tạo Secret này
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-api
                port:
                  number: 80

Cách 2: Certificate resource độc lập

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-example-com
  namespace: default
spec:
  secretName: api-example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - api.example.com
    - www.example.com

Debug khi cert không được issue

# Xem trạng thái Certificate
kubectl describe certificate api-example-com

# Xem CertificateRequest (giai đoạn trung gian)
kubectl get certificaterequest -n default

# Xem Order (ACME order)
kubectl get order -n default
kubectl describe order api-example-com-xxxxx

# Xem Challenge (HTTP-01 hay DNS-01)
kubectl get challenge -n default
kubectl describe challenge api-example-com-xxxxx

# Log của cert-manager controller
kubectl logs -n cert-manager deployment/cert-manager -f

Lỗi thường gặp:

LỗiNguyên nhânGiải pháp
Challenge pending mãiDNS chưa trỏ đúng hoặc cổng 80 bị chặnKiểm tra DNS và SG/firewall
Waiting for HTTP-01 challenge propagationIngress controller không route .well-known pathKiểm tra ingressClassName khớp và không bị middleware chặn
Rate limit exceededQuá nhiều request Let’s Encrypt prod (5 cert/tuần/domain)Dùng staging trước khi prod
no configured challenge solversSolver không match domain/ingressClassKiểm tra ClusterIssuer solvers selector
Order is invalidDomain không resolve tới cluster hoặc challenge fail nhiều lầnKiểm tra DNS A/CNAME và firewall
context deadline exceededcert-manager không kết nối được ACME serverKiểm tra egress, NAT, proxy

Debug tip: cert-manager tạo resource theo chuỗi CertificateCertificateRequestOrderChallenge. Lỗi thường nằm ở bước cuối (Challenge), kubectl describe challenge là lệnh hữu ích nhất.


ACME ARI và renewal jitter (2025+)

ARI (ACME Renewal Information) là extension mới cho phép CA gợi ý thời điểm renew tối ưu:

  • Let’s Encrypt hỗ trợ ARI từ 2024; cert-manager 1.15+ hỗ trợ đọc ARI.
  • Khi CA cần revoke sớm (ví dụ: CA incident), ARI báo cert-manager renew ngay thay vì chờ 60 ngày.
  • Giảm tình trạng “thundering herd” khi nhiều cert cùng renew lúc.

Renewal jitter: cert-manager 1.14+ thêm random jitter vào thời điểm renew (đến 10% của renewal window). Cluster 500+ cert không còn đồng loạt request ACME cùng giây.

# cert-manager 1.15+: ARI tự động bật khi CA hỗ trợ
# Kiểm tra Certificate status:
kubectl get certificate api-example-com -o yaml | grep -A5 renewalTime

HSTS, bật cẩn thận

HSTS (HTTP Strict Transport Security) nói với browser: “Chỉ kết nối HTTPS với domain này, trong X giây, và ghi nhớ.”

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Rủi ro:

  • Nếu bật includeSubDomains mà có subdomain chưa có HTTPS → toàn bộ subdomain đó không truy cập được.
  • Nếu bật preload và bị vào danh sách preload browser → không thể tắt trong vài tháng dù bạn muốn.
  • Rollback về HTTP trong thời gian max-agekhông thể với browser đã cache.

Khởi đầu an toàn:

max-age=300            # 5 phút, dễ rollback khi test
→ max-age=86400        # 1 ngày sau khi ổn
→ max-age=31536000     # 1 năm, thêm includeSubDomains khi chắc chắn

Tóm tắt

  • cert-manager tự động issue, renew và rotate cert, không cần cronjob thủ công.
  • HTTP-01: đơn giản nhưng cần HTTP công khai; DNS-01: linh hoạt hơn, dùng cho wildcard.
  • Debug theo thứ tự: CertificateCertificateRequestOrderChallenge.
  • HSTS với max-age lớn và preload là quyết định không dễ rollback, bắt đầu nhỏ.

Câu hỏi hay gặp

Wildcard *.example.com với Let’s Encrypt, HTTP-01 hay DNS-01?

Trả lời: DNS-01 (Let’s Encrypt không cấp wildcard qua HTTP-01). Cần issuer có solver DNS (Route53, Cloudflare…) và quyền tạo TXT _acme-challenge.

Certificate mãi Issuing, xem resource nào tiếp?

Trả lời: Chuỗi CertificateRequestOrderChallenge (kubectl describe challenge), log cert-manager, và kiểm tra DNS/HTTP reachability tới challenge.

Bật HSTS dài hạn + preload rồi cần HTTP port 80 lại, vì sao khó?

Trả lời: Browser ép HTTPS theo HSTS đã cache; preload khó revert. Tránh: bật max-age nhỏ khi thử nghiệm, hiểu rõ includeSubDomains/preload trước khi bật.


Bài tiếp theo (Giai đoạn IV): Quan sát mạng (Observability), sau khi hạ tầng mạng chạy, cần đo đạc để biết nó chạy tốt không.