chundev
日期:2026-05-22 環境:K3s + ingress-nginx + cert-manager
某個 web app 的 Ingress 用 cert-manager 的 selfsigned-issuer 發自簽憑證,瀏覽器一直跳憑證警告。解法是改用叢集裡既有的公開 CA 萬用憑證(*.example.com):把現成的 TLS Secret 複製到目標 namespace、Ingress 的 secretName 指過去、移除 cert-manager.io/cluster-issuer annotation。
三個值得記住的知識點:怎麼用 curl / openssl 在 30 秒內診斷 TLS 信任問題、Kubernetes 沒有原生跨 namespace 複製 Secret 的機制、移除 cert-manager annotation 不會自動清掉它先前建立的 Certificate。
| 方式 | Issuer 類型 | 瀏覽器是否信任 |
|---|---|---|
| 自簽 | selfSigned ClusterIssuer |
❌ 不信任,跳警告 |
| ACME | Let’s Encrypt 等公開 CA | ✅ 信任 |
| 既有憑證 | 手動匯入的 TLS Secret(如付費萬用憑證) | ✅ 信任(前提是 CA 公開) |
自簽憑證能把連線加密,但簽發它的 CA 不在任何信任清單裡,瀏覽器與 curl 都無法建立信任鏈。內部測試無所謂,但只要有人用瀏覽器實際打開就會看到警告。
當一個叢集多數 app 都已改用公開 CA 的萬用憑證、只剩少數還停在自簽,那少數就是該補的技術債。
ssl_verify_resultcurl 的 -w(write-out)可以印出底層 TLS 驗證結果,不必真的開瀏覽器:
curl -sS -m 15 -o /dev/null \
-w "HTTP %{http_code} | ssl_verify %{ssl_verify_result}\n" \
https://app.example.com/
ssl_verify 0 ✅ 憑證鏈完整且受信任ssl_verify 20 ❌ X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY——找不到簽發者,通常就是自簽或缺中繼憑證60(peer 憑證無法驗證)常見的 X509 verify error code:
| code | 意義 |
|---|---|
| 0 | 驗證通過 |
| 10 | 憑證已過期 |
| 18 | 自簽憑證(leaf 自己簽自己) |
| 19 | 憑證鏈裡有自簽的根,但不受信任 |
| 20 | 找不到本地簽發者憑證(缺中繼,或 CA 不受信任) |
⚠️ 加
-k/--insecure只是叫 curl 別驗證,會讓你「看不到」問題,不是修復。診斷階段請不要加。
echo | openssl s_client -connect app.example.com:443 \
-servername app.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
重點看三個欄位:
issuer= 是不是公開 CA(自簽的話 issuer 會等於 subject)notAfter= 到期日Subject Alternative Name 有沒有涵蓋你的網域萬用憑證 *.example.com 的 SAN 會是 DNS:*.example.com,只涵蓋單層子網域——app.example.com ✅,a.b.example.com ❌。
先確認別人都怎麼設定的,找出「落單」的那個:
kubectl get ingress -A -o custom-columns=\
'NS:.metadata.namespace,NAME:.metadata.name,HOST:.spec.rules[0].host,'\
'SECRET:.spec.tls[0].secretName,ISSUER:.metadata.annotations.cert-manager\.io/cluster-issuer'
一眼就能看出哪些還掛著 selfsigned-issuer、哪些已經改用共用的 wildcard-tls-secret。
Kubernetes 的 Secret 是 namespace-scoped 的,沒有原生的跨 namespace 複製指令。 萬用憑證 Secret 若已存在於其他 namespace,最乾淨的搬法是把憑證與私鑰取出、在目標 namespace 重建:
# 從來源 namespace 取出 cert / key
kubectl get secret wildcard-tls-secret -n other-app \
-o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/w.crt
kubectl get secret wildcard-tls-secret -n other-app \
-o jsonpath='{.data.tls\.key}' | base64 -d > /tmp/w.key
# 在目標 namespace 重建(dry-run 產生 manifest 再 apply,冪等)
kubectl create secret tls wildcard-tls-secret \
--cert=/tmp/w.crt --key=/tmp/w.key \
-n web --dry-run=client -o yaml | kubectl apply -n web -f -
rm -f /tmp/w.crt /tmp/w.key # 私鑰用完即刪
為什麼用 kubectl create ... --dry-run=client -o yaml | kubectl apply 而不是直接 kubectl get -o yaml | kubectl apply?因為直接 dump 出來的 manifest 帶有 namespace、resourceVersion、uid、managedFields 等綁定來源的欄位,apply 到別的 namespace 容易出錯。用 create tls 重新產生只含必要欄位,最乾淨。
長期方案: 若萬用憑證要被很多 namespace 共用、且會定期續期,手動複製會變成維護負擔。可以裝 Reflector 之類的 controller,在來源 Secret 加 annotation 就能自動同步到指定 namespace,續期時也會一起更新。
# k8s/base/ingress.yaml
metadata:
annotations:
# 移除這行 —— 不再用 cert-manager 自簽
# cert-manager.io/cluster-issuer: selfsigned-issuer
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- app.example.com
secretName: wildcard-tls-secret # 原本是 selfsigned-tls
兩個改動:移除 cert-manager.io/cluster-issuer annotation、secretName 指向萬用憑證 Secret。
kubectl apply -k ./k8s/overlays/production
# 驗證:ssl_verify 應為 0
curl -sS -o /dev/null \
-w "HTTP %{http_code} | ssl_verify %{ssl_verify_result}\n" \
https://app.example.com/
ingress-nginx 換 TLS Secret 是熱套用,幾秒內生效,不會重啟後端 Pod。
這是最容易忘的一步。cert-manager.io/cluster-issuer annotation 由 ingress-shim 這個子元件監看:它偵測到帶 annotation 的 Ingress,就會自動建立一個 Certificate 資源(名稱取自 tls.secretName)。
但反過來——把 annotation 移除,ingress-shim 並不會自動刪掉它先前建立的 Certificate。 那個 Certificate 連同其 CertificateRequest 會變成孤兒,cert-manager 還會繼續幫它續期。要手動清:
kubectl get certificate,certificaterequest -n web # 先看殘留
kubectl delete certificate selfsigned-tls -n web # 刪 Certificate(會連帶 CertificateRequest)
kubectl delete secret selfsigned-tls -n web # 刪舊的自簽 Secret
curl -w "%{ssl_verify_result}" 是診斷 TLS 信任問題最快的工具——0 受信任、20 找不到簽發者。不用開瀏覽器、不用裝東西。診斷時別加 -k,那只會遮住問題。create tls --dry-run | apply;長期共用考慮 Reflector 這類自動同步 controller。Certificate 會變孤兒並持續續期,必須手動刪。*.example.com 只涵蓋單層子網域。 多層子網域(a.b.example.com)不在 SAN 內。cert-manager.io/cluster-issuer 等 annotation 的完整定義ssl_verify_result