Webhook Certificate Management — CA Bundle & cert-manager
Vì sao một webhook chạy tốt lại "tự nhiên hỏng"
Một trong những sự cố admission khó hiểu nhất với người mới: webhook được triển khai, chạy hoàn hảo nhiều tháng, rồi một ngày mọi request bắt đầu bị từ chối mà không ai đụng vào cấu hình. Nguyên nhân gần như luôn giống nhau: chứng chỉ TLS của webhook đã hết hạn. Vì webhook là một HTTPS server và API server bắt buộc xác thực TLS khi gọi nó, một cert hết hạn nghĩa là API server không kết nối được → webhook coi như chết → và nếu failurePolicy: Fail (file 3), cluster mất khả năng ghi cho mọi request khớp rule.
Đây không phải vấn đề ngoại lệ — nó là vấn đề vòng đời tất yếu. Mọi cert đều có hạn. Nếu không có cơ chế tự động gia hạn và đồng bộ, webhook của bạn là một quả bom hẹn giờ với ngòi nổ là ngày hết hạn cert. File này giải thích chuỗi tin cậy TLS của webhook, cách caBundle hoạt động, và cách dùng cert-manager để biến vấn đề "cert hết hạn" thành phi-vấn-đề.
Lưu ý quan trọng: phần này chỉ áp dụng cho webhook tự viết. Nếu dùng ValidatingAdmissionPolicy (CEL, in-process — file 8), không có cert nào cả — policy chạy trong API server, không qua HTTPS. Đây là một trong những lý do mạnh nhất để ưu tiên CEL policy khi đủ biểu cảm: nó loại bỏ hoàn toàn lớp failure mode về cert.
Webhook là một HTTPS server — chuỗi tin cậy TLS
Khi API server gọi webhook, nó hành xử như một TLS client kết nối tới webhook (TLS server). Như mọi kết nối TLS, có hai phía của chuỗi tin cậy:
- Webhook server trình ra một serving certificate — chứng chỉ này phải hợp lệ cho DNS name mà API server dùng để gọi.
- API server verify serving cert đó bằng một CA certificate mà bạn cung cấp trong
caBundlecủa WebhookConfiguration.
Hai phía này phải khớp: serving cert phải được ký bởi CA mà caBundle chứa, và serving cert phải có đúng DNS name (SAN — Subject Alternative Name).
DNS name mà serving cert phải hợp lệ
API server gọi webhook qua một trong hai cách (file 2):
- Qua Service trong cluster (
clientConfig.service): API server resolve tới DNS nội bộ<service-name>.<namespace>.svc. Serving cert bắt buộc phải có SAN khớp đúng chuỗi này — ví dụwebhook-service.webhook-system.svc. Đây là dạng phổ biến nhất. - Qua URL ngoài (
clientConfig.url): serving cert phải hợp lệ cho hostname trong URL.
Đây là nguồn lỗi x509 thường gặp nhất: cert được tạo cho webhook-service.webhook-system.svc.cluster.local nhưng API server gọi webhook-service.webhook-system.svc (hoặc ngược lại), SAN không khớp, kết nối TLS thất bại. Tài liệu Kubernetes nêu rõ serving cert phải hợp lệ cho <svc_name>.<svc_namespace>.svc (webhook TLS). Thực hành an toàn: cert nên có cả hai dạng SAN (...svc và ...svc.cluster.local).
caBundle: cách API server biết tin ai
caBundle trong clientConfig là một chuỗi PEM được mã hóa base64 chứa CA certificate. API server dùng nó để verify serving cert mà webhook trình ra. Logic: "tôi sẽ tin serving cert nếu nó được ký bởi CA trong caBundle này".
Vấn đề vận hành cốt lõi: caBundle và serving cert phải luôn đồng bộ. Khi bạn xoay (rotate) serving cert sang một CA mới mà chưa cập nhật caBundle, API server vẫn dùng CA cũ để verify → verify thất bại → webhook chết. Đây chính là chỗ cert-manager + CA Injector giải quyết: tự động bơm caBundle đúng vào WebhookConfiguration mỗi khi CA thay đổi.
Tại sao tự quản cert là cái bẫy
Cách "thủ công" — tạo CA self-signed, ký serving cert, base64 và dán vào caBundle bằng tay (hoặc một script init) — chạy được lúc đầu nhưng sụp đổ theo thời gian:
- Cert hết hạn: không ai nhớ ngày hết hạn; webhook chết âm thầm.
- Rotate là thao tác đa bước dễ sai: tạo cert mới → cập nhật Secret webhook đọc → cập nhật caBundle trong mọi WebhookConfiguration trỏ tới nó → restart webhook để load cert mới. Sai thứ tự = downtime.
- Không quan sát được: không có cảnh báo "cert sắp hết hạn".
Vì vậy, mọi triển khai webhook production nên tự động hóa vòng đời cert. Cách chuẩn trên Kubernetes là cert-manager.
cert-manager + CA Injector — mô hình chuẩn
cert-manager là controller quản lý vòng đời chứng chỉ trên Kubernetes. Với webhook, nó giải hai bài toán cùng lúc:
1. Cấp và tự gia hạn serving cert
Bạn khai báo một Certificate object; cert-manager tạo cert, lưu vào Secret mà webhook server mount, và tự gia hạn trước khi hết hạn (mặc định gia hạn khi còn 1/3 thời hạn). Webhook không bao giờ gặp cert hết hạn nếu cert-manager khỏe.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: webhook-serving-cert
namespace: webhook-system
spec:
secretName: webhook-tls # webhook server mount Secret này
dnsNames:
- webhook-service.webhook-system.svc
- webhook-service.webhook-system.svc.cluster.local
issuerRef:
name: webhook-ca-issuer # Issuer dùng CA nội bộ (bên dưới)
kind: Issuer
duration: 8760h # 1 năm
renewBefore: 720h # gia hạn trước 30 ngày2. CA Injector tự bơm caBundle
Đây là phần giải quyết bài toán đồng bộ. cert-manager có một thành phần CA Injector (cainjector): khi bạn annotate một WebhookConfiguration, nó tự động bơm caBundle đúng vào mọi clientConfig của configuration đó, và cập nhật lại mỗi khi CA xoay.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-policy.example.com
annotations:
cert-manager.io/inject-ca-from: webhook-system/webhook-serving-cert
webhooks:
- name: pod-policy.example.com
clientConfig:
service:
namespace: webhook-system
name: webhook-service
path: /validate
# caBundle ĐỂ TRỐNG — cainjector tự bơm và tự cập nhật
# ...Annotation cert-manager.io/inject-ca-from: <namespace>/<certificate-name> bảo cainjector: "lấy CA từ Certificate này và bơm vào caBundle". Khi cert-manager xoay CA, cainjector cập nhật caBundle tự động — không downtime, không thao tác tay. Đây là cấu hình production khuyến nghị: bạn không bao giờ chạm vào caBundle bằng tay nữa.
Self-signed CA nội bộ vs CA tổ chức
Với admission webhook, CA không cần là CA công khai (public) — API server và webhook đều trong cùng cluster, nên một CA self-signed nội bộ là đủ và là lựa chọn phổ biến nhất:
# Issuer self-signed để bootstrap một CA
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-bootstrap
namespace: webhook-system
spec:
selfSigned: {}
---
# CA certificate (do selfsigned issuer ký)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: webhook-ca
namespace: webhook-system
spec:
isCA: true
commonName: webhook-ca
secretName: webhook-ca-secret
issuerRef:
name: selfsigned-bootstrap
kind: Issuer
---
# Issuer dùng CA trên để ký serving cert
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: webhook-ca-issuer
namespace: webhook-system
spec:
ca:
secretName: webhook-ca-secretChuỗi: selfsigned-bootstrap ký webhook-ca (CA) → webhook-ca-issuer dùng CA đó ký serving cert. API server tin serving cert vì caBundle (do cainjector bơm) chứa CA này.
Khi nào cần CA tổ chức (qua Google Cloud Certificate Authority Service hoặc CA doanh nghiệp): khi chính sách compliance yêu cầu mọi cert trong tổ chức phải truy nguyên về một root CA được kiểm soát tập trung. cert-manager hỗ trợ Google CAS issuer — bạn thay Issuer self-signed bằng issuer trỏ tới CAS. Với phần lớn webhook nội bộ, self-signed CA là đủ và đơn giản hơn.
Rotation không downtime — cơ chế
Để hiểu vì sao cert-manager + cainjector cho rotation không downtime, phải nắm thứ tự an toàn:
- cert-manager phát hiện serving cert sắp hết hạn (
renewBefore), cấp cert mới ký bởi cùng CA, ghi đè Secret. - Webhook server reload cert mới (cert-manager Secret được mount; cần webhook watch file hoặc restart — nhiều webhook framework hỗ trợ hot-reload).
- Vì CA không đổi, caBundle vẫn hợp lệ — API server vẫn verify được. Không cần cập nhật caBundle.
Khi xoay cả CA (hiếm hơn, ví dụ CA sắp hết hạn hoặc nghi lộ): cert-manager cấp CA mới, cainjector cập nhật caBundle trong WebhookConfiguration, serving cert mới ký bởi CA mới. Vì cainjector cập nhật caBundle tự động và gần như tức thời sau khi CA đổi, cửa sổ không khớp rất nhỏ. Để an toàn tuyệt đối, có thể dùng caBundle chứa cả CA cũ và mới trong giai đoạn chuyển tiếp (cert-manager hỗ trợ bundle nhiều CA).
Các lỗi x509 thường gặp và chẩn đoán
Khi webhook hỏng vì cert, lỗi thường xuất hiện trong audit log hoặc khi kubectl apply:
| Triệu chứng | Nguyên nhân | Khắc phục |
|---|---|---|
x509: certificate has expired or is not yet valid | Serving cert hết hạn | Kiểm tra cert-manager còn khỏe; cert-manager lẽ ra đã gia hạn — kiểm tra log cert-manager controller |
x509: certificate is valid for X, not <service>.<ns>.svc | SAN không khớp DNS name API server gọi | Đảm bảo Certificate dnsNames chứa đúng <service>.<ns>.svc |
x509: certificate signed by unknown authority | caBundle không chứa CA đã ký serving cert (lệch đồng bộ) | Kiểm tra annotation inject-ca-from; kiểm tra cainjector đang chạy |
| Webhook timeout / connection refused | Webhook Pod down, không phải lỗi cert | Kiểm tra webhook Deployment/Service (file 3) |
Lệnh chẩn đoán nhanh:
# Xem caBundle hiện tại trong WebhookConfiguration (đã base64)
kubectl get validatingwebhookconfiguration <name> \
-o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d | openssl x509 -noout -text
# Xem serving cert webhook đang trình ra (từ trong cluster)
kubectl run -it --rm certcheck --image=nicolaka/netshoot --restart=Never -- \
openssl s_client -connect webhook-service.webhook-system.svc:443 -showcerts
# Kiểm tra ngày hết hạn cert trong Secret
kubectl get secret webhook-tls -n webhook-system \
-o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -datesProduction architecture patterns
Pattern chuẩn: cert-manager + cainjector cho mọi webhook
Đây là cấu hình production mặc định và nên là khuôn cho mọi webhook tự viết: một CA self-signed nội bộ (hoặc CAS nếu compliance yêu cầu), Certificate cho serving cert với dnsNames đúng, annotation inject-ca-from trên WebhookConfiguration, webhook server hot-reload cert. Với cấu hình này, cert trở thành thứ bạn "cài một lần rồi quên".
Pattern thay thế: bỏ webhook, dùng CEL policy
Nếu policy biểu đạt được bằng CEL (file 8), cân nhắc bỏ webhook hoàn toàn. Không webhook = không cert = không cả lớp vấn đề này. Đây là lựa chọn kiến trúc ngày càng được khuyến nghị cho policy validating thuần.
Giám sát: cảnh báo trước khi hết hạn
Dù cert-manager tự gia hạn, vẫn nên có một lớp an toàn: cảnh báo nếu bất kỳ cert nào còn dưới N ngày. cert-manager expose metric certmanager_certificate_expiration_timestamp_seconds; dựng alert trên Cloud Monitoring (xem Chương 14 về observability) để được báo nếu gia hạn thất bại — phòng trường hợp cert-manager bị kẹt.
Common mistakes / anti-patterns
- Tự sinh cert bằng script init, không gia hạn — chạy được lúc deploy, chết sau 1 năm. Luôn dùng cert-manager.
- SAN thiếu dạng
.svc— cert chỉ có.svc.cluster.local(hoặc ngược lại). Đặt cả hai trongdnsNames. - Cập nhật caBundle bằng tay — dễ lệch đồng bộ khi rotate. Để cainjector làm.
- Không giám sát cert-manager — nếu chính cert-manager kẹt (lỗi RBAC, issuer hỏng), cert không được gia hạn mà không ai biết. Cảnh báo trên metric hết hạn.
- Webhook không hot-reload cert — cert-manager ghi cert mới vào Secret nhưng webhook vẫn giữ cert cũ trong bộ nhớ. Đảm bảo webhook watch file cert hoặc có cơ chế reload.