Sự cố chứng chỉ hết hạn lúc 3 giờ sáng
Sau khi server của tôi bị tấn công brute-force SSH vào lúc nửa đêm, bảo mật trở thành mối quan tâm hàng đầu trong mỗi dự án mới. Vì vậy khi tôi gia nhập một nhóm đang chạy Kubernetes trên môi trường production, câu hỏi đầu tiên tôi đặt ra là: “Ai đang quản lý chứng chỉ SSL của các bạn?” Câu trả lời là một Google Sheet dùng chung ghi ngày hết hạn và một bot nhắc nhở trên Slack. Ba tuần sau, một chứng chỉ hết hạn vào cuối tuần khiến cổng thanh toán ngừng hoạt động suốt sáu tiếng đồng hồ.
Sáu tiếng downtime. Vì một cert mà ai cũng biết sắp hết hạn. Đó là lúc tôi nhận ra: quản lý chứng chỉ thủ công ở quy mô lớn không phải là một quy trình — đó là một đồng hồ đếm ngược mà bạn quên không theo dõi.
Tại sao quản lý chứng chỉ thủ công thất bại
Vấn đề không phải do lười biếng. Mà là độ phức tạp tăng nhanh hơn bất kỳ bảng tính nào có thể theo kịp.
Một cluster nhỏ có thể có 10 service dùng TLS. Môi trường production cỡ vừa dễ dàng có đến 50–100 chứng chỉ trải rộng trên nhiều namespace, nhiều domain, các service nội bộ và wildcard cert. Mỗi cái có ngày hết hạn riêng, cửa sổ gia hạn riêng, CA cấp phát riêng và vị trí secret riêng. Không ai theo dõi được hết — không mắc lỗi là may, và chắc chắn không thể qua được một kỳ nghỉ cuối tuần dài.
Ba kiểu lỗi cứ lặp đi lặp lại mãi:
- Bỏ lỡ gia hạn — Nhắc nhở bắn ra, bị chôn vùi trong Slack, và không ai xử lý kịp trước chiều thứ Sáu.
- Tên Secret không khớp — Ai đó cập nhật cert thủ công nhưng tên Kubernetes Secret không khớp với những gì Ingress controller mong đợi. Lưu lượng bị gián đoạn mà không có cảnh báo.
- CA nội bộ bị trôi dạt — Các service nội bộ dùng CA tự ký, nhưng bản thân cert CA hết hạn sau 2 năm. Không ai theo dõi.
Tất cả những điều này đều có thể phòng tránh bằng tự động hóa.
Bạn có những lựa chọn nào?
cert-manager không phải công cụ duy nhất ở đây. Hiểu rõ các lựa chọn thay thế giúp bạn dễ đưa ra quyết định hơn.
Lựa chọn 1: Certbot trên từng node
Cách tiếp cận kinh điển. Chạy certbot renew dưới dạng cron job trên mỗi VM. Hoạt động tốt với server tĩnh, nhưng không phù hợp với Kubernetes. Chứng chỉ tồn tại ngoài cluster và cần đồng bộ thủ công vào Secret. Nếu một pod khởi động lại và mount Secret cũ, bạn sẽ phải debug lỗi TLS vào đúng lúc tệ nhất.
Lựa chọn 2: External secrets + Vault
HashiCorp Vault có thể cấp phát và gia hạn chứng chỉ thông qua PKI secrets engine. Rất xuất sắc cho doanh nghiệp lớn với cấu trúc CA phức tạp. Tuy nhiên với nhóm nhỏ hơn, bản thân Vault cần được deploy, tăng cường bảo mật, mở khóa và bảo trì — chi phí vận hành đáng kể trước khi bạn nhận được bất kỳ lợi ích nào.
Lựa chọn 3: cert-manager
cert-manager chạy bên trong cluster của bạn như một Kubernetes-native controller. Nó theo dõi các custom resource (Certificate, Issuer, ClusterIssuer) và xử lý toàn bộ vòng đời: yêu cầu, cấp phát, lưu trữ dưới dạng Kubernetes Secret, và tự động gia hạn. Không cần thao tác thủ công. Nó hỗ trợ Let’s Encrypt (ACME), CA nội bộ, Vault và Venafi sẵn có.
Với hầu hết các nhóm chạy Kubernetes, cert-manager là lựa chọn hiển nhiên. Nó tích hợp tự nhiên với Ingress annotation và gần như không cần bảo trì gì sau khi đã chạy.
Cài đặt cert-manager từng bước
Bước 1: Cài cert-manager
Dùng Helm chart chính thức. Bạn cần Kubernetes 1.22+ và Helm 3.
# Thêm Helm repo của Jetstack
helm repo add jetstack https://charts.jetstack.io
helm repo update
# Cài cert-manager kèm CRDs
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
Trước khi tiếp tục, xác nhận các pod đang hoạt động tốt:
kubectl get pods -n cert-manager
Bạn sẽ thấy ba pod: cert-manager, cert-manager-cainjector và cert-manager-webhook, tất cả ở trạng thái Running. Nếu pod webhook bị kẹt, hãy đợi 60 giây — nó khởi tạo sau cùng.
Bước 2: Cấu hình ClusterIssuer với Let’s Encrypt
ClusterIssuer có phạm vi toàn cluster. Còn Issuer thông thường chỉ có phạm vi trong namespace. Với các service public, Let’s Encrypt với HTTP-01 challenge là điểm khởi đầu đơn giản nhất.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Apply rồi kiểm tra trạng thái:
kubectl apply -f clusterissuer-letsencrypt.yaml
kubectl get clusterissuer letsencrypt-prod -o yaml
Tìm Ready: True trong phần status conditions. Nếu bị kẹt, kiểm tra log của controller: kubectl logs -n cert-manager deploy/cert-manager. Chín phần mười là do gõ sai email hoặc server URL.
Bước 3: Cấp chứng chỉ đầu tiên qua Ingress Annotation
Chỉ cần thêm một annotation vào Ingress của bạn. cert-manager phát hiện nó và tự động tạo resource Certificate — không cần thêm manifest nào khác.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: my-app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
Trong vòng 60–90 giây, cert-manager tạo Secret my-app-tls trong namespace production. Theo dõi quá trình này theo thời gian thực:
kubectl get certificate -n production -w
kubectl describe certificate my-app-tls -n production
Việc gia hạn tự động diễn ra khi chứng chỉ còn 30 ngày trước khi hết hạn (với cert Let’s Encrypt 90 ngày). Bạn không cần động tay vào nữa.
Bước 4: CA nội bộ cho các service nội bộ
Các microservice nội bộ dùng mTLS cũng cần chứng chỉ. Chúng không nên dùng Let’s Encrypt — cái đó dành cho domain public. Thay vào đó hãy dùng private CA issuer.
Tạo root CA trước:
# Tạo private key cho CA
openssl genrsa -out ca.key 4096
# Tạo chứng chỉ CA (có hiệu lực 10 năm)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=Internal Cluster CA/O=MyOrg"
Lưu vào Kubernetes Secret:
kubectl create secret tls internal-ca-secret \
--cert=ca.crt \
--key=ca.key \
-n cert-manager
Tạo ClusterIssuer dựa trên CA đó:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-secret
Các service nội bộ giờ có thể yêu cầu chứng chỉ từ CA nội bộ của bạn bằng cùng một pattern resource Certificate — chỉ cần trỏ internal-ca là issuer.
Bước 5: Resource Certificate tường minh cho workload không dùng Ingress
Một số service không bao giờ đụng đến Ingress — gRPC backend, PostgreSQL dùng TLS, API nội bộ. Với những service đó, tạo resource Certificate trực tiếp:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: postgres-tls
namespace: database
spec:
secretName: postgres-tls-secret
issuerRef:
name: internal-ca
kind: ClusterIssuer
dnsNames:
- postgres.database.svc.cluster.local
duration: 720h # 30 ngày
renewBefore: 168h # gia hạn trước 7 ngày khi hết hạn
Giám sát tình trạng chứng chỉ
Tự động hóa xử lý việc gia hạn, nhưng bạn vẫn cần khả năng quan sát. cert-manager mặc định phơi ra Prometheus metrics. Bắt đầu với một cuộc kiểm tra nhanh trên tất cả namespace:
# Liệt kê tất cả chứng chỉ
kubectl get certificates --all-namespaces
# Đánh dấu những cái chưa ở trạng thái Ready
kubectl get certificates --all-namespaces | grep -v True
Để cảnh báo, thêm một Prometheus rule kích hoạt khi chứng chỉ còn dưới 14 ngày trước khi hết hạn mà chưa được gia hạn:
- alert: CertificateExpirationWarning
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 1209600
for: 1h
labels:
severity: warning
annotations:
summary: "Chứng chỉ sẽ hết hạn trong vòng chưa đầy 14 ngày"
14 ngày cho bạn hai tuần làm việc đầy đủ để điều tra mà không cần xử lý khẩn cấp.
Những sai lầm thường gặp cần tránh
- Dùng staging Let’s Encrypt trên production — Endpoint ACME staging (
https://acme-staging-v02.api.letsencrypt.org/directory) cấp chứng chỉ mà trình duyệt không tin tưởng. Dùng nó để test, rồi đổi server URL trước khi đưa lên production. - HTTP-01 đằng sau firewall — HTTP-01 yêu cầu domain của bạn có thể truy cập công khai trên cổng 80. Các cluster riêng tư hoặc môi trường air-gapped cần dùng DNS-01 challenge thay thế — cert-manager hỗ trợ Route53, Cloudflare và nhiều nhà cung cấp khác.
- Nhầm lẫn phạm vi Issuer và ClusterIssuer —
Issuerthông thường chỉ hoạt động trong namespace của nó. Tham chiếu từ namespace khác sẽ gặp lỗi “issuer not found” khó hiểu. Khi không chắc, hãy dùngClusterIssuer. - Quên theo dõi ngày hết hạn của CA nội bộ — cert-manager quản lý các chứng chỉ lá mà nó cấp phát. Nó sẽ không cảnh báo bạn khi bản thân cert CA hết hạn. Đặt lịch nhắc nhở cho ngày hết hạn CA, hoặc kết nối Prometheus alert ở trên để phát hiện sớm.
Kết quả: Quản lý chứng chỉ hoàn toàn tự động
Sau khi triển khai điều này trên toàn cluster, việc quản lý chứng chỉ biến mất khỏi danh sách công việc định kỳ. Không còn bảng tính. Không còn nhắc nhở Slack. Không còn bị gọi vào cuối tuần.
Alert Prometheus kích hoạt hai lần trong tám tháng. Cả hai lần đều là do các service chúng tôi tạm thời chuyển ra ngoài sự kiểm soát của cert-manager. Cả hai lần đều phát hiện ra với hơn 10 ngày còn lại — đủ thời gian để xử lý bình tĩnh trong giờ làm việc.
Cài đặt rất tối giản: cài cert-manager, tạo một ClusterIssuer cho domain public và một cái cho service nội bộ, thêm một annotation vào Ingress resource của bạn. Việc gia hạn, cập nhật Secret và reload Ingress đều diễn ra mà không cần bạn can thiệp. Công việc của bạn trở thành xem xét thỉnh thoảng một alert thay vì đi săn ngày hết hạn.
Đó là một trạng thái tốt hơn nhiều để ở vào lúc 3 giờ sáng.

