Share Notes

chundev

View the Project on GitHub latteouka/share-notes

K3s 環境的 TLS 憑證管理:自簽、萬用、Let’s Encrypt 完整指南

日期:2026-03-20 環境:K3s + ingress-nginx + cert-manager


TL;DR

K3s 環境有兩套完全獨立的 TLS 憑證體系:K3s 內部憑證(叢集元件通訊)和應用憑證(Ingress HTTPS)。前者由 K3s 自動管理,後者需要你自己處理。應用憑證有三種管理方式:手動上傳萬用憑證、cert-manager 自簽、cert-manager + Let’s Encrypt。本文整理三種方案的設定方式、自動更新機制、以及 ingress-nginx 如何做到零停機憑證熱載入。


背景

在 K3s 上跑 Web 應用,對外提供 HTTPS 服務時,會遇到一個看似簡單但細節很多的問題:憑證從哪來、怎麼更新、過期了怎麼辦?

很多人搞混 K3s 自己的憑證和應用的 TLS 憑證,以為 K3s 會自動幫 Ingress 處理 HTTPS — 不會的。這篇從架構上釐清兩者的分工,再逐一說明三種應用憑證方案。


兩套憑證體系:K3s 內部 vs 應用層

這是最容易搞混的地方,先釐清:

面向 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

文件中常出現的「Leaf Certificate」(也叫 End-Entity Certificate)指的是憑證信任鏈最末端的憑證。名稱來自樹狀結構 — leaf 在最末端,下面不會再簽發其他憑證:

Root CA(根憑證)                    ← 自簽,最上層,10 年
  └── Intermediate CA(中繼憑證)
        └── Leaf Certificate         ← 最末端,發給具體服務,365 天

在 K3s 的語境中:


K3s 內部憑證的自動輪替

機制

K3s 在每次啟動時檢查所有 Leaf Certificate(client/server certificates),若距離到期 120 天以內,自動續期。

⚠️ 版本注意:2025 年 5 月之前的版本(v1.33.1+k3s1、v1.32.5+k3s1 之前),觸發門檻是 90 天而非 120 天。

重點

手動操作

# 檢查所有憑證狀態
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 -

Ingress 引用

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 包裝驗證和部署流程,包含:

  1. 檢查憑證檔案存在
  2. 驗證憑證與私鑰匹配(比對 modulus MD5)
  3. 檢查是否過期
  4. 部署到多個 namespace
  5. 設定 CronJob 定期檢查到期日並發送告警

應用憑證方案二:cert-manager 自簽憑證

適合內部環境、開發測試、或不需要瀏覽器信任的場景。

前置條件

安裝 cert-manager 並建立 SelfSigned ClusterIssuer:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}

Certificate 資源

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 天更新)。

進階:Bootstrap CA 模式(推薦)

直接用 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,所有服務的憑證都會被信任。

優缺點


應用憑證方案三:cert-manager + Let’s Encrypt

適合對外服務,需要瀏覽器自動信任的場景。

HTTP-01 vs DNS-01

特性 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 天然支援

ClusterIssuer 設定(HTTP-01)

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

ClusterIssuer 設定(DNS-01,以 Cloudflare 為例)

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

Certificate 資源(Wildcard 範例)

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 憑證熱載入機制

不管用哪種方案,憑證更新後 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 生效

關鍵技術:Lua 動態憑證選取

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 / 購買憑證 內網、開發測試 對外正式服務

學到的事


參考資料