事件時間線:4/19 遷移 → 4/27 renew → 5/5 才發現
- 4/19:web server 從 nginx 切換到 IIS。原 nginx-era 的
scripts/post-renew.ps1沒同步重寫,仍含Restart-Service nginx邏輯。Scheduled TaskSSL-PostRenew每天 06:00 跑,每天 exit 1 失敗,但沒人看 LastResult。 - 4/27 08:32:Let's Encrypt 自動 renew,新 cert 進 LocalMachine\My,win-acme 用 IIS install plugin 把 cert 綁定到 IIS site binding。但 cert ↔ private key 的 schannel metadata(KeyProvInfo)沒設好——cert 安裝成功,private key 在 LocalMachine\Crypto\RSA 內,但 schannel TLS handshake 找不到對應 key file。
- 4/27 之後 8 天:所有 HTTPS connection TLS handshake reset。Cloudflare cache 部分頁面繼續 serve(看起來像「偶爾壞」),origin 直連完全壞。
- 5/5 18:42:使用者體感到網站進不去回報。19:30 修好——
certutil -repairstore My <thumbprint>重建 cert ↔ key metadata 關聯,schannel 立即生效,IIS 不需重啟。
為什麼 8 天才被發現?三道警戒線同時失效
事件後做 postmortem,列每一道應該攔下的機制:
- 沒有外部 uptime monitoring。沒有任何 service 從外網每幾分鐘 ping 首頁。
- Sentry 監測 SDK 載不到。HTTPS handshake 階段就 reset,瀏覽器根本沒下載 JS 檔案,error tracking SDK 從未啟動。「網站壞了 Sentry 會通知」這條假設依賴 SDK 至少能載入。
- 開發人員 hosts override 直接訪問 origin。dev 機器把
www.kmau.com.tw指向內網 IP,origin 沒裝對外的 cert,所以 dev 自己訪問本來就 https 失敗。「真壞」跟「平常 hosts 失敗」沒分別。 - Cloudflare cache 部分 stale 繼續 serve。某些 user 看到舊頁面,不一定意識到網站已壞。Cache hit 反而蓋住了完全失效的事實。
- Scheduled Task LastResult 純背景。SSL-PostRenew 每天 exit 1,但沒 alert pipeline,沒人定期看 schtasks。
每一道都是「正常情況下會發現」的機制,但都依賴前一道工作。一旦多道同時失效,故障靜默降級的時間 = MTTR。
Root cause:cert install ≠ schannel ready
真正 root cause 不是 nginx 殘留 script——那是觸發點,不是真原因。真原因是:
win-acme 的 IIS install plugin 把 cert 安裝到 LocalMachine\My,更新 IIS site binding,但沒可靠地設定 schannel 需要的 KeyProvInfo metadata(PROV_INFO 結構)。
cert 跟 private key 在憑證儲存區看起來都在,但 schannel 找不到 cert 對應的 key file。對 IIS 來說 binding 看起來正常,對 schannel TLS 處理來說整個 handshake 用不出 key 來簽——直接 RST 連線。
certutil -repairstore 工具重建這個 metadata,把 cert thumbprint 跟 RSA key file 在 KeyProvInfo 上關聯起來。修完後 schannel 立即可用,不需要重啟 IIS。
三層防護方案(彼此獨立、互為 backup)
Layer 1:外部 uptime monitoring(5 分鐘 alert)
用 UptimeRobot / Pingdom / Better Uptime 等外部服務每 5 分鐘對首頁做 HTTPS HEAD。任何 5xx / TLS handshake reset / connection refused 立即 email + 推播。
關鍵特性:服務在外網、不依賴自家網站任何元件——即使整個 origin 都炸了也照常 alert。免費版即可,不需要付費版的「cert expiry 提前 30 天通知」(HTTPS check 在 cert 到期當天 5 分鐘內也會 alert,本來就 cover)。
從 8 天 → 5 分鐘,這一道就把事件影響範圍縮 99.96%。
Layer 2:主機端 hourly self-healing
Scheduled Task 每小時跑 idempotent 腳本:
- 找
LocalMachine\My內最新(NotAfter 最遠)的 cert - probe RSA private key accessibility(嘗試讀 KeyProvInfo)
- 不通就
certutil -repairstore My <thumbprint> - 用
netsh http show sslcert驗證 IIS binding(避免 hairpin NAT 直接做 outbound HTTPS) - 失敗就 POST 到
/api/error-report→ Sentry alert
worst case 從 24 小時降到 1 小時。注意是 idempotent——成功狀態下重跑 100 次也不副作用。
Layer 3:ACME post-renew Script hook(root cause fix)
真正解決問題的不是 mitigation,是從根源消除 metadata 不一致。win-acme 的 renewal config 加 Script install plugin(與 IIS install plugin 並列):
{
"InstallationPluginOptions": [
{ "Plugin": "iis", ... },
{ "Plugin": "script",
"Script": "C:\\inetpub\\kmauWeb\\scripts\\post-renew.ps1",
"ScriptParameters": "-Thumbprint {CertThumbprint}" }
]
}
renew 後 win-acme 會依序:(1)IIS plugin 安裝 cert + 更新 binding、(2)Script plugin 立刻跑 post-renew.ps1 修 metadata。0 秒延遲,不靠 hourly safety net。
這層才是 root fix——下次 cert renew 完成那一刻 metadata 就對了。Layer 1 跟 Layer 2 變成「萬一 Layer 3 也壞了」的多重保險。
Mitigation vs Root fix:差別在哪?
事件當天為了讓網站立刻好,跑 certutil -repairstore 是 mitigation——治了當下,沒解未來。下次 renew 還會壞。
真 root fix 是讓 renew 的那一刻就修好,不靠 hourly poll。差別:
| 方案 | 修復延遲 | 覆蓋未來 | 類型 |
|---|---|---|---|
| 事件當下 certutil -repairstore | 0(只當下) | 不 | Mitigation |
| Layer 2 hourly self-healing | ≤ 1h | 是 | Safety net |
| Layer 3 win-acme Script hook | 0 | 是 | Root fix |
| Layer 1 UptimeRobot | 5 min(alert,不修) | 是 | Detection |
三層各司其職:Detection(你發現) + Self-healing(系統自修) + Root fix(從源頭不再壞)。任何一層失效,其他兩層仍 cover。
架構遷移後的審查 SOP
事件追到底,根因是「nginx → IIS 遷移時沒清舊架構的 Scheduled Task / script」。post-renew.ps1 從遷移那天起 16 天每天都失敗,但失敗對 happy-path(cert 沒 renew)沒影響。直到 4/27 renew 觸發新路徑才壞。
未來架構切換的審查 SOP:
- 列出舊架構所有 Scheduled Task / cron / hook script(一個都不能漏)
- 逐個評估:刪除 / 重寫 / 保留(含解釋為何保留)
- 砍 service 同時砍 file(如
C:\nginx\直接 rename 成.deprecated.YYYYMMDD) - 切換後 prod 至少跑一次真實的 hot path(如手動 renew 一張 cert),不只看 happy path
- Postmortem 寫進 runbook,下次同類遷移直接照單檢查
給其他企業 IT 主管的 5 個 takeaway
重點摘要
- 外部 uptime monitoring 不選——任何企業網站都該有。免費版每 5 分鐘 HTTPS check 就把 MTTR 從天級降到分級,沒理由不裝。
- Sentry / 自家 monitoring 不能當唯一警戒線——它依賴你的網站還能載 JS。HTTPS handshake 階段壞了,Sentry 不會通知你。
- 「自動化」不等於「不會壞」——LE 自動續期是過程自動化,不保證結果正確。每個自動化都要有 fail-safe + alert。
- 架構遷移必審 legacy script——切換 web server / DB / framework 時,舊架構的 cron / Scheduled Task 通常被忘記。「砍 service 順手砍 script」是流程紀律。
- Mitigation 是急救,不是治療——事件後一定要追到 root cause,否則下次同樣方式會再壞。
需要協助設計企業網站的 SSL / TLS 監測、ACME post-renew hook、或事件後 postmortem 流程?凱茂提供包含 Layer 1-3 完整方案的網站維運諮詢。
預約 30 分鐘免費網站維運架構諮詢 →