chundev
日期:2026-03-20 環境:K3s + ingress-nginx + cert-manager
K3s 環境有兩套完全獨立的 TLS 憑證體系:K3s 內部憑證(叢集元件通訊)和應用憑證(Ingress HTTPS)。前者由 K3s 自動管理,後者需要你自己處理。應用憑證有三種管理方式:手動上傳萬用憑證、cert-manager 自簽、cert-manager + Let’s Encrypt。本文整理三種方案的設定方式、自動更新機制、以及 ingress-nginx 如何做到零停機憑證熱載入。
在 K3s 上跑 Web 應用,對外提供 HTTPS 服務時,會遇到一個看似簡單但細節很多的問題:憑證從哪來、怎麼更新、過期了怎麼辦?
很多人搞混 K3s 自己的憑證和應用的 TLS 憑證,以為 K3s 會自動幫 Ingress 處理 HTTPS — 不會的。這篇從架構上釐清兩者的分工,再逐一說明三種應用憑證方案。
這是最容易搞混的地方,先釐清:
| 面向 | K3s 內部憑證 | 應用憑證(Ingress TLS) |
|---|---|---|
| 管理者 | K3s 內建機制 | 你自己(或 cert-manager) |
| 用途 | API server ↔ kubelet、etcd、controller-manager 等叢集元件通訊 | 使用者瀏覽器 ↔ Ingress 的 HTTPS |
| 儲存位置 | 節點檔案系統 /var/lib/rancher/k3s/server/tls/ |
Kubernetes Secret (kubernetes.io/tls) |
| CA 來源 | K3s 啟動時自動產生的自簽 CA | Let’s Encrypt、手動上傳的萬用憑證、或 cert-manager 自簽 |
| 有效期 | Leaf Certificate 365 天 / CA 10 年 | 視方案而定(通常 90 天 ~ 1 年) |
| 過期後果 | 叢集掛掉(kubectl 不通、Pod 無法調度) | 應用 HTTPS 失效(叢集本身正常) |
關鍵:兩者互不影響。 cert-manager 是跑在 K3s 裡的 workload,它依賴 K3s 憑證正常才能運作,但 K3s 完全不管 cert-manager 簽發的憑證。
文件中常出現的「Leaf Certificate」(也叫 End-Entity Certificate)指的是憑證信任鏈最末端的憑證。名稱來自樹狀結構 — leaf 在最末端,下面不會再簽發其他憑證:
Root CA(根憑證) ← 自簽,最上層,10 年
└── Intermediate CA(中繼憑證)
└── Leaf Certificate ← 最末端,發給具體服務,365 天
在 K3s 的語境中:
K3s 在每次啟動時檢查所有 Leaf Certificate(client/server certificates),若距離到期 120 天以內,自動續期。
⚠️ 版本注意:2025 年 5 月之前的版本(v1.33.1+k3s1、v1.32.5+k3s1 之前),觸發門檻是 90 天而非 120 天。
CertificateExpirationWarning 事件# 檢查所有憑證狀態
k3s certificate check --output table
# 輪替Leaf Certificate(每個節點都要做,先 server 再 agent)
systemctl stop k3s
k3s certificate rotate
systemctl start k3s
# 輪替 CA(慎用,會影響整個叢集)
k3s certificate rotate-ca
確保 K3s 至少每年重啟一次(系統更新、安全性修補通常就會觸發),或設定排程定期檢查:
# 簡單的 crontab 檢查腳本
k3s certificate check --output table 2>&1 | grep -E "EXPIRED|CLOSE"
適合已經有購買萬用憑證(如 *.example.com)的場景。
# 將憑證檔案建立為 K8s TLS Secret
kubectl create secret tls wildcard-tls-secret \
--cert=fullchain.pem \
--key=privkey.crt \
-n my-namespace \
--dry-run=client -o yaml | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: wildcard-tls-secret # ← 指向手動建立的 Secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80
可以用 Makefile 包裝驗證和部署流程,包含:
適合內部環境、開發測試、或不需要瀏覽器信任的場景。
安裝 cert-manager 並建立 SelfSigned ClusterIssuer:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-tls
namespace: my-namespace
spec:
secretName: web-tls-secret # cert-manager 自動建立並維護此 Secret
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
dnsNames:
- app.example.com
duration: 2160h # 90 天
renewBefore: 360h # 到期前 15 天開始更新
Ingress 的 tls.secretName 指向 web-tls-secret,cert-manager 會自動填入憑證內容。
cert-manager controller 持續監控所有 Certificate 資源:
Certificate 建立
→ cert-manager 立即簽發,建立 Secret(tls.crt + tls.key)
→ 計算 renewalTime = notAfter - renewBefore
→ 持續監控...
→ 到達 renewalTime
→ 產生新的 key pair
→ 用 Issuer 簽發新憑證
→ 覆寫 Secret 內容(同名)
→ ingress-nginx 偵測到 Secret 變更 → 熱載入
預設行為:如果沒指定 renewBefore,cert-manager 會在憑證有效期的 2/3 處開始更新(例如 90 天憑證在第 60 天更新)。
直接用 SelfSigned Issuer 簽發的憑證,每張都是獨立的自簽憑證,沒有共同的信任根。推薦先 bootstrap 出一個 CA:
# 1. 用 SelfSigned 簽一張 CA 憑證
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-ca
namespace: cert-manager
spec:
isCA: true
secretName: my-ca-secret
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
commonName: "My Internal CA"
duration: 87600h # 10 年
---
# 2. 建立 CA Issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: my-ca-issuer
spec:
ca:
secretName: my-ca-secret
---
# 3. 後續憑證都用 CA Issuer 簽發
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-tls
spec:
secretName: web-tls-secret
issuerRef:
name: my-ca-issuer # ← 用 CA,不是 selfsigned
kind: ClusterIssuer
dnsNames:
- app.example.com
好處:所有憑證共用一個 CA,只要客戶端信任這個 CA,所有服務的憑證都會被信任。
適合對外服務,需要瀏覽器自動信任的場景。
| 特性 | HTTP-01 | DNS-01 |
|---|---|---|
| 驗證方式 | Let’s Encrypt 存取 http://domain/.well-known/acme-challenge/ |
建立 _acme-challenge.domain TXT 記錄 |
| 需要 Port 80 對外 | 是 | 否 |
| Wildcard 支援 | 否 | 是(唯一方式) |
| 需要 DNS API | 否 | 是(Cloudflare、Route53、Google CloudDNS 等) |
| 多節點環境 | 需確保 challenge 路由到正確 Pod | 天然支援 |
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: admin@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: admin@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
需要先建立 Cloudflare API Token 的 Secret:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=YOUR_CLOUDFLARE_API_TOKEN \
-n cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: my-namespace
spec:
secretName: wildcard-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- "example.com" # 裸域名也要加
duration: 2160h
renewBefore: 720h # 到期前 30 天更新
不管用哪種方案,憑證更新後 ingress-nginx 都能零停機載入新憑證,不需要重啟 Pod。
Secret 內容更新(cert-manager 覆寫 or kubectl apply)
→ K8s informer 偵測到 Secret update 事件
→ ingress-nginx controller syncSecret()
→ 提取 tls.crt + tls.key
→ 寫入 /etc/ingress-controller/ssl/<ns>-<secret>.pem
→ 更新 SSLCertTracker(記憶體內的 registry)
→ 更新 Lua shared dictionary
→ 下一個 TLS handshake 生效
ingress-nginx 使用 OpenResty 的 Lua 模組,在 TLS handshake 時動態選取憑證:
Client 發送 ClientHello(帶 SNI hostname)
→ Lua 從 ngx.var.ssl_server_name 取得 hostname
→ 查詢 Lua shared dictionary 找對應憑證
→ ngx.ssl.set_cert() + ngx.ssl.set_priv_key()
→ 完成 handshake
這代表 NGINX 不需要 reload config,連線也不會中斷。
| 手動萬用憑證 | cert-manager 自簽 | cert-manager + Let’s Encrypt | |
|---|---|---|---|
| 瀏覽器信任 | ✅ 是 | ❌ 不安全警告 | ✅ 是 |
| 自動更新 | ❌ 手動 | ✅ 全自動 | ✅ 全自動 |
| Wildcard | ✅ 是 | ✅ 是 | ✅ DNS-01 / ❌ HTTP-01 |
| 成本 | 需購買 | 免費 | 免費 |
| 外部依賴 | 無 | 無 | Let’s Encrypt + DNS API |
| 適用場景 | 企業已有 PKI / 購買憑證 | 內網、開發測試 | 對外正式服務 |