Một nền tảng tạo website demo theo yêu cầu, mỗi site một subdomain dưới cùng một tên miền gốc, sẽ sớm gặp lỗi này: Certificate mãi ở Ready=False và trong describe có dòng 429 urn:ietf:params:acme:error:rateLimited ... too many certificates already issued. Let's Encrypt giới hạn số chứng chỉ cấp cho một tên miền gốc trong một khoảng thời gian, và hàng chục subdomain demo chạm trần rất nhanh. Giải pháp hợp lý là một chứng chỉ wildcard dùng chung cho mọi site. Tài liệu này nói về phía cluster: chart Helm hai nhánh, một lỗi khoảng trắng trong template, cách đồng bộ chứng chỉ vào nhiều namespace và phương án đúng hướng hơn với cert-manager. Phần cấp chứng chỉ bằng certbot được giải thích ở SSL cho hàng trăm domain với Certbot.
Triệu chứng: Certificate Ready=False và lỗi 429
kubectl get certificate -n site-a
NAME READY SECRET AGE
site-a-certificate False letsencrypt-secret-site-a 28m
kubectl describe certificate site-a-certificate -n site-a
Message: ... Failed to create Order: 429 urn:ietf:params:acme:error:rateLimited:
Error creating new order :: too many certificates already issued for "example.com"cert-manager vẫn thử lại theo chu kỳ, nên site sẽ có HTTPS khi hạn mức được làm mới. Nhưng với nền tảng cần site chạy ngay sau khi tạo, chờ nhiều ngày là không chấp nhận được; chứng chỉ wildcard tránh được tình huống này. Con số hạn mức thay đổi theo chính sách, nên hãy đọc trang Rate Limits của Let's Encrypt thay vì nhớ một con số cố định.
Vì sao demo dễ chạm trần, và chứng chỉ wildcard giải quyết thế nào
Mỗi site demo xin một chứng chỉ riêng cho x.example.com và www.x.example.com. Đếm theo tên miền đăng ký, tất cả đều tính vào cùng một hạn mức. Chứng chỉ wildcard cho *.example.com chỉ cần xin một lần, rồi dùng cho mọi subdomain, nên số lần xin không còn tăng theo số site.
Giới hạn quan trọng: chứng chỉ wildcard chỉ phủ một cấp tên. *.example.com khớp demo.example.com nhưng không khớp www.demo.example.com. Template cũ tự động thêm host www.<domain> sẽ cho ra một host nằm ngoài SAN, và trình duyệt báo lỗi chứng chỉ ở host đó. Vì vậy ở nhánh dùng chứng chỉ wildcard cần bỏ host www đi.
Chứng chỉ wildcard bắt buộc dùng DNS-01
Let's Encrypt chỉ cấp wildcard qua thử thách DNS-01: bạn chứng minh quyền kiểm soát tên miền bằng một bản ghi TXT tại _acme-challenge.example.com. Chế độ thủ công của certbot cho phép xin chứng chỉ wildcard, và cái giá của nó là không tự gia hạn được:
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d "*.example.com"Lưu ý cú pháp: mỗi tên miền một cờ -d. Certbot dừng lại, yêu cầu bạn thêm bản ghi TXT, và chờ bạn bấm Enter. Chứng chỉ wildcard có hạn 90 ngày, và vì có bước thao tác tay nên certbot renew không chạy tự động được. Đây là lý do việc gia hạn tự động chứng chỉ wildcard thường vẫn nằm trong danh sách việc chưa xong của nhiều hệ thống. Cách tự động hoá DNS-01 bằng cert-manager nằm ở phần phương án bên dưới.
Chart Helm hai nhánh: tls.alreadyAvailable
Để cùng một chart phục vụ cả site dùng chứng chỉ wildcard lẫn site tự xin chứng chỉ, chart có hai nhánh. Values:
tls:
alreadyAvailable: true
crt_base64: "<noi-dung-fullchain-base64>"
key_base64: "<noi-dung-privkey-base64>"Khi alreadyAvailable bật, chart tạo một Secret kiểu kubernetes.io/tls:
{{- if .Values.tls.alreadyAvailable }}
apiVersion: v1
kind: Secret
metadata:
name: tls-secret-{{ include "web.fullname" . }}
type: kubernetes.io/tls
data:
tls.crt: {{ .Values.tls.crt_base64 | quote }}
tls.key: {{ .Values.tls.key_base64 | quote }}
{{- end }}Khi tắt, chart tạo Certificate để cert-manager xin chứng chỉ riêng:
{{- if not .Values.tls.alreadyAvailable }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "web.fullname" . }}-certificate
spec:
secretName: letsencrypt-secret-{{ include "web.fullname" . }}
renewBefore: 360h # gia hạn trước 15 ngày
issuerRef:
name: letsencrypt-issuer
kind: ClusterIssuer
dnsNames:
- {{ .Values.domain }}
- www.{{ .Values.domain }}
{{- end }}Ingress cần trỏ secretName đúng Secret của nhánh đang dùng. Nền tảng chart và Helm nói chung được giải thích ở Helm chart là gì.
Bẫy trim marker: secretName dính vào host
Đoạn ingress tưởng đơn giản này sinh ra YAML vẫn hợp lệ nhưng nội dung sai:
tls:
- hosts:
- www.{{ .Values.domain }}
- {{ .Values.domain }}
{{- if .Values.tls.alreadyAvailable -}}
secretName: tls-secret-{{ $fullName }}
{{- else -}}
secretName: letsencrypt-secret-{{ $fullName }}
{{- end }}Kết quả sau khi render, host bị nối với secretName và Ingress mất secretName:
- site-a.example.comsecretName: tls-secret-site-a-example-comNguyên nhân nằm ở dấu gạch ngang. Trong Go template, {{- xoá mọi khoảng trắng, kể cả xuống dòng, phía trước thẻ; -}} xoá khoảng trắng phía sau. Ở {{- if ... -}} dấu gạch thứ hai nuốt luôn dòng mới và phần thụt lề trước secretName, nên secretName bị kéo lên dính vào cuối dòng host. Cách sửa là chỉ giữ gạch ở phía trước:
{{- if .Values.tls.alreadyAvailable }}
secretName: tls-secret-{{ $fullName }}
{{- else }}
secretName: letsencrypt-secret-{{ $fullName }}
{{- end }}Dạng {{- else }} giữ được xuống dòng sau thẻ, nên secretName nằm đúng dòng riêng với đúng thụt lề. Quy tắc nhớ: dùng -}} rất hiếm, chỉ khi thật sự muốn dính hai dòng. Lỗi này khó bắt: dòng sai vẫn là một chuỗi hợp lệ trong danh sách hosts, nên helm lint và --dry-run phía client (mặc định ở Helm 3.13 trở lên) đều không báo gì. Hãy đọc kết quả helm template và validate phía server, nơi API server từ chối tên host chứa dấu hai chấm và dấu cách:
helm template site-a ./chart -f values.yaml | less
helm template site-a ./chart -f values.yaml | kubectl apply --dry-run=server -f -
kubectl get ingress -n site-a -o yaml # so sánh sau khi deployChi tiết cú pháp ở tài liệu Helm: Flow Control.
Đồng bộ chứng chỉ wildcard vào Secret của nhiều namespace
Khi chứng chỉ wildcard được cấp và gia hạn ở một nơi (ví dụ máy chạy certbot), cần đưa nó vào Secret TLS của từng namespace site. Một pipeline làm việc này gồm các bước: lấy fullchain.pem và privkey.pem, mã hoá base64, duyệt các namespace theo nhãn, cập nhật Secret, rồi khởi động lại deployment. Ý tưởng đúng, nhưng kịch bản kiểu sed cần chỉnh nhiều chỗ:
- Sửa YAML bằng
seddễ hỏng. Regex có dấu chấm dễ khớp quá rộng,greplấy tên Secret có thể trả về nhiều dòng khi namespace có nhiều Secret TLS, và cờbase64khác nhau giữa GNU (-w 0) và macOS/BSD nên kịch bản chạy được trên máy này có thể hỏng trên máy khác. Đây là rủi ro chung, chưa kiểm chứng trên một pipeline cụ thể. Cách chắc chắn hơn là tạo lại Secret từ tệp:
kubectl create secret tls tls-secret-site-a \
--cert=fullchain.pem --key=privkey.pem \
-n site-a --dry-run=client -o yaml | kubectl apply -f -Tên Secret chứa chứng chỉ wildcard phải trùng giá trị secretName mà Ingress tham chiếu. Nếu Secret do Helm tạo, kubectl apply sẽ cảnh báo thiếu annotation, đó chính là xung đột ở ý thứ hai bên dưới.
- Restart deployment thường là thừa. Nếu TLS kết thúc ở ingress controller, controller tự nạp lại Secret khi nó đổi; ứng dụng phía sau không đọc chứng chỉ. Chỉ cần restart khi chính pod ứng dụng đọc tệp chứng chỉ.
- Secret do Helm tạo mà pipeline sửa ngoài Helm. Lần
helm upgradekế tiếp sẽ ghi đè Secret về giá trị trong values. Hãy chọn một nguồn sự thật duy nhất. - Khoá riêng nằm trong values và lịch sử release. Helm lưu values của mỗi revision trong Secret của release, nên ai đọc được Secret trong namespace có thể lấy lại khoá. Đây là rủi ro cần cân nhắc khi nhét
key_base64vào values. - Biến CI chứa kubeconfig và khoá SSH phải được che (masked) và hạn chế phạm vi; không in ra log.
Phương án đúng hướng hơn
Ba cách tránh phải tự đồng bộ chứng chỉ wildcard bằng kịch bản:
- cert-manager với DNS-01: để cấp chứng chỉ wildcard tự động, khai một
ClusterIssuerdùng solver DNS-01 với nhà cung cấp DNS có API (Cloudflare chẳng hạn), rồi tạoCertificatecódnsNames: ["*.example.com"]. cert-manager tự tạo bản ghi TXT và tự gia hạn trước hạn. Xem ingress-nginx và cert-manager: HTTPS tự động trên Kubernetes cho phần cài đặt nền. Mẫu rút gọn:apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-dns01 spec: acme: server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: { name: letsencrypt-account-key } solvers: - dns01: cloudflare: apiTokenSecretRef: { name: cloudflare-api-token, key: api-token } --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: wildcard-example-com spec: secretName: wildcard-example-com issuerRef: { name: letsencrypt-dns01, kind: ClusterIssuer } dnsNames: ["*.example.com"] - Một Certificate wildcard, sao chép Secret: tạo một
Certificateduy nhất ở một namespace, rồi dùng công cụ sao chép Secret giữa namespace như reflector để mỗi site tham chiếu cùng một Secret. - Secret mặc định của ingress-nginx: đặt cờ
--default-ssl-certificate=<namespace>/<secret>cho controller để mọi Ingress không khai TLS dùng chung một Secret wildcard, không cần sao chép sang từng namespace.
Các phương án cấp chứng chỉ wildcard này là đề xuất theo tài liệu, chưa được thử. Tài liệu: cert-manager: DNS01. Bài chưa kiểm chứng hai điểm: kịch bản đồng bộ ở trên có chạy đúng trên cluster của bạn không, và Secret do cert-manager tạo có bị xoá khi chuyển tls.alreadyAvailable sang true hay không (phụ thuộc --enable-certificate-owner-ref và ownerReferences của Secret). Hãy tự thử bằng kubectl get secret -o yaml trên namespace test.
Kiểm tra chứng chỉ wildcard sau khi đổi
echo | openssl s_client -connect site-a.example.com:443 \
-servername site-a.example.com 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName -enddateCờ -servername là bắt buộc để gửi SNI, nếu không ingress trả chứng chỉ mặc định. Kiểm tra SAN có *.example.com, và ngày hết hạn đúng như chứng chỉ wildcard mới.
Checklist
- Dùng chứng chỉ wildcard khi số subdomain lớn; nhớ nó chỉ phủ một cấp tên, bỏ host
wwwở nhánh này. - Chứng chỉ wildcard cần DNS-01; thủ công thì không tự gia hạn, hãy tự động hoá bằng cert-manager.
- Đọc kết quả
helm templatevà validate phía server (kubectl apply --dry-run=server) trước mỗi lần đổi template;helm lintkhông bắt được lỗi này; tránh-}}. - Dùng
kubectl create secret tls --dry-run=client -o yaml | kubectl apply -f -thay vìsed. - Không để Secret chứng chỉ wildcard vừa do Helm quản lý vừa do pipeline sửa.
- Xác minh bằng
openssl s_client -servername: SAN và hạn dùng.
Bài tiếp theo
HPA cho Laravel: ngưỡng theo request, behavior scale và metrics-server. Chuyển từ chứng chỉ sang co giãn: để số pod tự tăng khi tải tăng.



