Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Kubernetes Ingress 從自簽憑證遷移到萬用憑證

日期:2026-05-22 環境:K3s + ingress-nginx + cert-manager


TL;DR

某個 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


背景:cert-manager 的三種發憑證方式

方式 Issuer 類型 瀏覽器是否信任
自簽 selfSigned ClusterIssuer ❌ 不信任,跳警告
ACME Let’s Encrypt 等公開 CA ✅ 信任
既有憑證 手動匯入的 TLS Secret(如付費萬用憑證) ✅ 信任(前提是 CA 公開)

自簽憑證能把連線加密,但簽發它的 CA 不在任何信任清單裡,瀏覽器與 curl 都無法建立信任鏈。內部測試無所謂,但只要有人用瀏覽器實際打開就會看到警告。

當一個叢集多數 app 都已改用公開 CA 的萬用憑證、只剩少數還停在自簽,那少數就是該補的技術債。


症狀:如何快速診斷 TLS 信任問題

curl:看 ssl_verify_result

curl 的 -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/

常見的 X509 verify error code:

code 意義
0 驗證通過
10 憑證已過期
18 自簽憑證(leaf 自己簽自己)
19 憑證鏈裡有自簽的根,但不受信任
20 找不到本地簽發者憑證(缺中繼,或 CA 不受信任)

⚠️ 加 -k / --insecure 只是叫 curl 別驗證,會讓你「看不到」問題,不是修復。診斷階段請不要加。

openssl:看實際送出的憑證鏈

echo | openssl s_client -connect app.example.com:443 \
  -servername app.example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

重點看三個欄位:

萬用憑證 *.example.com 的 SAN 會是 DNS:*.example.com只涵蓋單層子網域——app.example.com ✅,a.b.example.com ❌。


解法:遷移到既有的萬用憑證

步驟 1 — 盤點全叢集 Ingress 的 TLS 設定

先確認別人都怎麼設定的,找出「落單」的那個:

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

步驟 2 — 把萬用憑證 Secret 複製到目標 namespace

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 帶有 namespaceresourceVersionuidmanagedFields 等綁定來源的欄位,apply 到別的 namespace 容易出錯。用 create tls 重新產生只含必要欄位,最乾淨。

長期方案: 若萬用憑證要被很多 namespace 共用、且會定期續期,手動複製會變成維護負擔。可以裝 Reflector 之類的 controller,在來源 Secret 加 annotation 就能自動同步到指定 namespace,續期時也會一起更新。

步驟 3 — 修改 Ingress

# 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。

步驟 4 — 套用並驗證

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。

步驟 5 — 清理 cert-manager 殘留(容易漏掉)

這是最容易忘的一步。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

學到的事


參考資料