chundev
日期:2026-03-28 技術棧:Next.js / K8s ingress-nginx / cert-manager
Web 應用新增了 TLS 憑證上傳功能,管理員可在 /settings/tls 上傳自訂憑證取代 cert-manager 自簽憑證。本文記錄完整的測試計畫:先用刻意產生的錯誤檔案驗證每一層防線,再用公司正式域名做端對端實測。
憑證上傳有三道防線,每一道都需要獨立驗證:
使用者上傳檔案
↓
① 前端預檢(檔案大小、副檔名、PEM header)
↓
② 後端驗證(cert/key 配對、過期、域名比對、chain)
↓
③ K8s 部署 + Health Check(更新 Secret/Ingress → tls.connect 驗證)
↓
成功 or 自動回退
mkdir -p test-certs && cd test-certs
# 1. 不是 PEM 格式的 .pem 檔(測前端 PEM header 檢查)
echo "這不是PEM格式的檔案" > not-pem.pem
# 2. 有 PEM header 但內容損壞(測後端解析)
echo "-----BEGIN CERTIFICATE-----
AAAA不合法的base64內容
-----END CERTIFICATE-----" > bad-content.pem
# 3. 過期憑證(測後端過期檢查)
openssl genrsa -out expired.key 2048
openssl req -new -key expired.key -subj "/CN=test.example.com" -out expired.csr
openssl x509 -req -in expired.csr -signkey expired.key \
-not_before 20240101000000Z -not_after 20240102000000Z \
-extfile <(echo "subjectAltName=DNS:test.example.com") \
-out expired.pem
rm expired.csr
# 4. 域名不符的合法憑證(測後端域名比對)
openssl req -x509 -newkey rsa:2048 \
-keyout wrong-domain.key -out wrong-domain.pem \
-days 365 -nodes \
-subj "/CN=wrong.example.com" \
-addext "subjectAltName=DNS:wrong.example.com"
# 5. 合法憑證 + 不配對的 key(測後端 key mismatch)
openssl req -x509 -newkey rsa:2048 \
-keyout valid.key -out valid.pem \
-days 365 -nodes \
-subj "/CN=test.example.com" \
-addext "subjectAltName=DNS:test.example.com"
openssl genrsa -out mismatched.key 2048
進入 /settings/tls 頁面,依序測試:
| # | 上傳什麼 | 域名填什麼 | 預期結果 | 擋在哪一層 |
|---|---|---|---|---|
| 1 | not-pem.pem 當憑證 |
— | Toast:不是有效的 PEM 格式 | ① 前端 |
| 2 | 改副檔名的 .txt 檔 |
— | Toast:不支援的檔案格式 | ① 前端 |
| 3 | bad-content.pem + 任意 key |
test.example.com |
錯誤:無法解析憑證內容 | ② 後端 |
| 4 | valid.pem + mismatched.key |
test.example.com |
錯誤:憑證與私鑰不配對 | ② 後端 |
| 5 | expired.pem + expired.key |
test.example.com |
錯誤:憑證已過期 | ② 後端 |
| 6 | wrong-domain.pem + wrong-domain.key |
test.example.com |
錯誤:CN/SAN 不包含域名 | ② 後端 |
| 7 | valid.pem + valid.key |
test.example.com |
驗證通過 → Health check 失敗 → 自動回退 | ③ K8s |
| 8 | 不填任何東西 | — | 套用按鈕 disabled | ① 前端 |
案例 7 說明: cert 和 key 都合法且域名也對,但
test.example.com的 DNS 沒指向 K8s,所以tls.connecthealth check 會連不上 → 系統自動回退到前一組憑證,頁面顯示 Alert 警告。
要讓整個流程跑到底(包括 health check 通過),需要:
| 條件 | 說明 | 怎麼確認 |
|---|---|---|
| 🔑 TLS 憑證 | 公司 CA 簽發或購買的憑證 | .pem 格式,包含 server cert + 中繼 CA chain |
| 🔑 私鑰 | 對應的私鑰 | .pem 或 .key 格式 |
| 🌐 DNS | 域名 A record 指向 K8s Ingress IP | nslookup <domain> 解析到正確 IP |
| 🔧 ENCRYPTION_KEY | Pod 環境變數(備份加密用) | 已有 SMTP 功能在用,應該已設定 |
1. 準備憑證
如果有正式憑證:
# 確認 cert 和 key 配對
openssl x509 -in cert.pem -noout -modulus | md5
openssl rsa -in key.pem -noout -modulus | md5
# 兩個 hash 要一樣
# 確認 cert chain 完整
openssl verify -CAfile chain.pem cert.pem
# 合併 chain(如果 cert 和 chain 是分開的)
cat cert.pem chain.pem > fullchain.pem
如果想先用自簽憑證測流程(不用正式憑證):
# 用公司域名產生自簽 cert
openssl req -x509 -newkey rsa:2048 \
-keyout test.key -out test.pem \
-days 365 -nodes \
-subj "/CN=your-domain.com" \
-addext "subjectAltName=DNS:your-domain.com"
⚠️ 自簽憑證可以測試整個流程(K8s 更新、Ingress 切換),但瀏覽器會顯示不受信任警告。
2. 設定 DNS
# 確認 DNS 已指向 K8s Ingress IP
nslookup your-domain.com
# 應該解析到 K8s 節點 IP
# 如果是內部測試,可以先改 /etc/hosts
echo "<K8S_NODE_IP> your-domain.com" | sudo tee -a /etc/hosts
⚠️ DNS 必須先設好再上傳憑證。因為 health check 會嘗試
tls.connect到域名的 443 port,如果 DNS 解析不到 → health check 失敗 → 自動回退。
3. 上傳憑證
/settings/tlsyour-domain.com)fullchain.pem(或 test.pem)key.pem(或 test.key)三階段 loading:驗證中... → 套用中... → 檢查中...
4. 驗證結果
套用成功後,狀態卡片應該更新:
命令列驗證:
# 確認 K8s Secret 已更新
kubectl get secret <tls-secret-name> -n <namespace> -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -subject -dates
# 確認 Ingress host 已更新
kubectl get ingress <ingress-name> -n <namespace> -o jsonpath='{.spec.rules[0].host}'
# 從外部驗證 TLS
echo | openssl s_client -connect your-domain.com:443 -servername your-domain.com 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates
5. 測試回退
| 項目 | 說明 |
|---|---|
| ingress-nginx cache | 極少數情況下更新 Secret 後 Lua cache 不刷新,需重啟 ingress controller Pod |
| Health check 目標 | 連到域名的 443 port,離線環境需確保 DNS 可解析 |
| 單域名限制 | 目前只支援一個域名 + 一組憑證(Ingress 只有一個 rule) |
| 瀏覽器快取 | 切換憑證後瀏覽器可能快取舊 TLS session,需要清除或開無痕視窗 |
openssl s_client 預設不回報驗證失敗 — 要加 -verify_return_error 才會在憑證驗證失敗時中斷連線