# SOP:更新 Caddy SSL 憑證(`tel.tpech.gov.tw`) 本 SOP 用於把新申請到的 SSL 憑證部署到 `/opt/caddy/`,讓 `tel.tpech.gov.tw` 使用新憑證。 > 本 SOP 只處理 **Caddy** 的憑證(`tel.tpech.gov.tw`)。 > Ant Media Server 的 `tel-server.tpech.gov.tw` 是**另一張獨立憑證**,更新方式見 [ssl.md](ssl.md)。 --- ## 實戰紀錄(2026-04-16 首次執行) 以下是本 SOP 首次實際執行時踩到的坑,記錄下來避免日後重蹈覆轍。 ### 坑 1:TWCA cert.zip 裡的副檔名是 `.cer`,不是 `.crt` TWCA 核發的壓縮檔解開是 `root.cer`、`uca.cer`、`server.cer`,都是 PEM 格式但副檔名為 `.cer`。指令中的 `*.crt` 要改成 `*.cer`,否則找不到檔案。 ### 坑 2:Private Key 不在主機上 主機 `/opt/caddy/privkey.pem` 裡面是舊的 HiPKI 那把 key。這次向 TWCA 申請時用的是**新產生的 CSR / key pair**,所以舊 key 和新憑證的 modulus 不匹配。 最後在本機 `~/Downloads/ssl/tel/tel.key`(2026-03-26 產生)找到正確的 key。 **教訓**:申請憑證時,key 和 CSR 一定要放到固定位置並標註日期與用途。建議統一放到 `ssl-staging/<網域>-<年月>/` 並保留 `.key` + `.csr` 原始檔。 ### 坑 3:Docker 指令需要 sudo `tpech_1duan` 使用者不在 docker group 裡,`./restart.sh` 必須加 `sudo`。 ### 坑 4:醫院前端有防火牆 / WAF 做 TLS 終端 Caddy 重啟後,**主機本地** `127.0.0.1:443` 驗證是新的 TWCA 憑證,但**從外部**連 `106.105.181.40:443` 看到的還是舊的 HiPKI 憑證。 原因:醫院網路在伺服器前面有一層設備(防火牆 / WAF / load balancer)在做 SSL 終端或 SSL inspection。外部 TLS 連線是跟那台設備交握,不是直接跟 Caddy 交握。 **需要額外通知網管單位**:把新憑證也更新到前端防火牆/WAF 上。憑證檔案可以從主機 `/opt/caddy/hospital/ssl_tel_tpech_gov_tw_2026_10/` 取得。 ### 坑 5:OCSP Stapling timeout Caddy log 中出現 TWCA 的 OCSP 伺服器 `twcasslocsp.twca.com.tw` 連線 timeout。原因可能是主機對外存取被限制。這只是 warning,不影響 TLS 正常運作,但客戶端可能需要自行查 OCSP。 --- ## 憑證資訊 ### 目前生效的憑證(2026-04-16 部署) | 項目 | 值 | |------|-----| | 網域 | `tel.tpech.gov.tw` | | 簽發者 | TWCA Secure SSL Certification Authority | | 憑證序號 | `95590185165637134231279949412783532535` | | 生效日 | 2026-04-15 | | 到期日 | **2026-10-30**(僅半年) | | 憑證檔 | `cert.zip`(內含 `root.cer`、`uca.cer`、`server.cer`) | | 私鑰來源 | 本機 `~/Downloads/ssl/tel/tel.key`(2026-03-26 產生) | | 主機備份 | `/opt/caddy/hospital/ssl_tel_tpech_gov_tw_2026_10/` | ### 被替換的舊憑證 | 項目 | 值 | |------|-----| | 簽發者 | 中華電信 HiPKI OV TLS CA - G1 | | 有效期 | 2025-04-23 ~ 2026-04-23 | | 主機備份 | `/opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_2026_10_backup/` | > **注意**:這張憑證只有半年效期,**2026-10 月前**必須再次更新。 --- ## 0. 前置檢查(動手前必確認) | # | 檢查項 | 指令 / 說明 | |---|--------|------------| | A | **cert.zip** | TWCA 寄來的附件。解壓後應有 3 個 `.cer` 檔 | | B | **Private Key** | 當初產生 CSR 時留下的私鑰。**TWCA 不會寄這個**,必須自己保留 | | C | **SSH 可連** | `ssh hospital_media "echo ok"` | | D | **Caddy 活著** | `ssh hospital_media "sudo docker ps \| grep caddy"` | > **找不到私鑰怎麼辦?** > > 1. 先搜本機:`find ~/Downloads ~/Desktop ~/Documents -name '*.key' -newer ` > 2. 再搜主機:`ssh hospital_media "find /opt /root /home -maxdepth 4 -name '*.key' 2>/dev/null"` > 3. 都找不到 → 重新產生 CSR/key,向 TWCA 申請 rekey(重核發) --- ## 1. 本機準備工作目錄 工作區放在 repo 下的 `ssl-staging/`(已加入 `.gitignore`,不會 commit)。 ```bash cd /Users/timmy/Projects/Freelance/106_105_181_40 mkdir -p ssl-staging/tel-tpech-<年月> # 例:tel-tpech-2026-10 cd ssl-staging/tel-tpech-<年月> ``` 把 `cert.zip` 和 `privkey.pem`(或 `*.key`)都放進來。 --- ## 2. 解壓 ```bash unzip cert.zip ls # TWCA 格式:root.cer uca.cer server.cer # (注意副檔名是 .cer,不是 .crt) ``` --- ## 3. 檢查憑證格式與內容 ```bash # 確認是 PEM 文字格式 file *.cer # 應為 "PEM certificate" 或 "ASCII text" # 確認網域、簽發者、效期 openssl x509 -in server.cer -noout -subject -issuer -dates ``` ### 若是 DER 格式(`file` 顯示 "data" 或 "ASN.1"),轉 PEM: ```bash for f in root.cer uca.cer server.cer; do openssl x509 -inform DER -in "$f" -out "$f.pem" && mv "$f.pem" "$f" done ``` --- ## 4. 合成 fullchain.pem 並命名私鑰 ```bash # 合併:server 在前,中繼 CA 在後(順序不可反) cat server.cer uca.cer > fullchain.pem # 私鑰統一命名 cp <你的key檔> privkey.pem # 例:cp tel.key privkey.pem ls privkey.pem # 確認存在 ``` > root.cer 不需放進 fullchain.pem,Caddy 只需 leaf + intermediate 就夠。 --- ## 5. 驗證「憑證」與「私鑰」是否匹配 **最重要的一步。錯了貼上去 Caddy 會起不來。** ```bash openssl x509 -in fullchain.pem -noout -modulus | openssl md5 openssl rsa -in privkey.pem -noout -modulus | openssl md5 ``` 兩個 MD5 **必須完全相同**。 ``` # 正確範例(兩邊一樣): # MD5(stdin)= 1c4ae9b6fae3920bb30f76ac792db989 # MD5(stdin)= 1c4ae9b6fae3920bb30f76ac792db989 ``` 若不同 → **立刻停**。代表這把 key 不是產生 CSR 的那把。回頭找對的 key,參考步驟 0 的搜尋方法。 > **2026-04-16 實例**:主機上的 `/opt/caddy/privkey.pem`(舊 HiPKI 那把)modulus 為 `c4b56aed...`,新憑證為 `1c4ae9b6...`,不匹配。最後在本機 `~/Downloads/ssl/tel/tel.key` 找到匹配的。 --- ## 6. 備份主機上的舊憑證 ```bash ssh hospital_media "sudo mkdir -p /opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_<年月>_backup && \ sudo cp /opt/caddy/fullchain.pem /opt/caddy/privkey.pem \ /opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_<年月>_backup/ && \ ls -l /opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_<年月>_backup/" ``` --- ## 7. 上傳新憑證到主機 ```bash # 經由 /tmp 中轉,避免權限問題 scp fullchain.pem privkey.pem hospital_media:/tmp/ ssh hospital_media "sudo mv /tmp/fullchain.pem /tmp/privkey.pem /opt/caddy/ && \ sudo chmod 644 /opt/caddy/fullchain.pem && \ sudo chmod 600 /opt/caddy/privkey.pem && \ ls -l /opt/caddy/fullchain.pem /opt/caddy/privkey.pem" ``` --- ## 8. 重啟 Caddy ```bash # 注意:需要 sudo,否則 permission denied ssh hospital_media "cd /opt/caddy && sudo ./restart.sh" ``` 確認 log 無報錯: ```bash ssh hospital_media "sudo docker logs caddy --tail 20" # 看到 "serving initial configuration" 且無 error 就算成功 # OCSP stapling timeout 是 warning,可忽略 ``` --- ## 9. 驗證 ### 9a. 主機本地驗證(必做,最可靠) ```bash ssh hospital_media "openssl s_client -connect 127.0.0.1:443 \ -servername tel.tpech.gov.tw /dev/null \ | openssl x509 -noout -subject -issuer -dates" # 預期: # subject= ... CN=tel.tpech.gov.tw # issuer= ... TWCA ... # notAfter= ... ``` **看到新的 issuer 和到期日 → Caddy 端完成。** ### 9b. 外部驗證(可能看到舊憑證!) ```bash openssl s_client -connect tel.tpech.gov.tw:443 \ -servername tel.tpech.gov.tw /dev/null \ | openssl x509 -noout -subject -issuer -dates ``` > **已知問題**:醫院前端有防火牆 / WAF 做 TLS 終端,從外面連進去看到的可能還是舊憑證。 > 這不是 Caddy 的問題——需要**另外通知網管單位**更新防火牆/WAF 上的憑證。 > 見下方「[防火牆 / WAF 注意事項](#防火牆--waf-注意事項)」。 --- ## 10. 歸檔到主機備份目錄 ```bash scp cert.zip fullchain.pem privkey.pem server.cer uca.cer root.cer \ hospital_media:/tmp/ ssh hospital_media "sudo mkdir -p /opt/caddy/hospital/ssl_tel_tpech_gov_tw_<年月> && \ sudo mv /tmp/cert.zip /tmp/fullchain.pem /tmp/privkey.pem \ /tmp/server.cer /tmp/uca.cer /tmp/root.cer \ /opt/caddy/hospital/ssl_tel_tpech_gov_tw_<年月>/ && \ ls -l /opt/caddy/hospital/ssl_tel_tpech_gov_tw_<年月>/" ``` --- ## 11. 通知網管 + 更新文件 ### 通知網管更新防火牆/WAF 發信或工單給網管單位,內容: > `tel.tpech.gov.tw:443` 的 SSL 憑證已在伺服器端更新為 TWCA 新憑證(到期 YYYY-MM-DD),請同步更新防火牆 / 負載均衡器上的 TLS 終端憑證。 > 新憑證檔案位置:主機 `/opt/caddy/hospital/ssl_tel_tpech_gov_tw_<年月>/` ### 更新本 repo 文件 把以下文件裡 `tel.tpech.gov.tw` 的 CA 與到期日更新: - [docs/caddy.md](caddy.md) — `Caddy SSL 憑證` 段 - [docs/ssl.md](ssl.md) — `憑證總覽` - [docs/maintenance.md](maintenance.md) — `SSL 憑證到期時間` - [SUMMARY.md](../SUMMARY.md) — 第 6 節 --- ## 防火牆 / WAF 注意事項 `106.105.181.40` 的 `:443` 前面有醫院的網路設備做 TLS 終端: ``` 外部用戶 → [防火牆/WAF :443 (TLS 終端)] → [106.105.181.40 Caddy :443] ``` - Caddy 裝了新憑證,只能讓「防火牆 → Caddy」這段用新憑證。 - 「外部用戶 → 防火牆」那段用的是**防火牆上安裝的憑證**,需要網管另外處理。 - 驗證是否全程更新完成:從外部執行 `openssl s_client`,看到新的 issuer 才算全部搞定。 --- ## 回滾流程(萬一 Caddy 起不來) ```bash ssh hospital_media "sudo cp /opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_<年月>_backup/fullchain.pem \ /opt/caddy/hospital/ssl_tel_tpech_gov_tw_pre_<年月>_backup/privkey.pem \ /opt/caddy/ && \ cd /opt/caddy && sudo ./restart.sh" ``` 回滾後檢查第 5 步的 MD5 是否真的一致、第 4 步的 `cat` 順序是否正確。 --- ## 常見地雷 | 症狀 | 通常原因 | 參考步驟 | |------|----------|----------| | Caddy 起來但 `ERR_SSL_PROTOCOL_ERROR` | 憑證/私鑰不匹配 | 第 5 步 | | `NET::ERR_CERT_AUTHORITY_INVALID` | `fullchain.pem` 少了中繼 CA | 第 4 步 | | `tls: failed to parse private key` | 私鑰格式不對(加密了 / DER 沒轉 PEM) | 第 3 步 | | `scp` 上去後 Caddy 讀不到 | 權限不對(644/600) | 第 7 步 | | `./restart.sh` permission denied | 忘了加 `sudo` | 第 8 步 | | 外部仍看到舊憑證、本地是新的 | 醫院防火牆/WAF TLS 終端未更新 | 第 9b 步 | | OCSP stapling timeout(log warning) | 主機對外連線受限,不影響功能 | 可忽略 | | 主機上的 `privkey.pem` 和新憑證不匹配 | 申請新憑證時用了新 CSR/key,舊 key 當然不配 | 第 0/5 步 | --- ## 主機備份目錄結構(供日後查閱) ``` /opt/caddy/hospital/ ├── ssl_tel_tpech_gov_tw_2025/ # 2025 HiPKI 憑證原件 ├── ssl_tel_tpech_gov_tw_pre_2026_10_backup/ # 2026-04 更新前的舊檔備份 ├── ssl_tel_tpech_gov_tw_2026_10/ # 2026 TWCA 憑證原件 ├── ssl_tel_server_tpech_gov_tw_2025/ # tel-server 2025 憑證 └── ... ```