問題描述
我們的系統架構是這樣的:在 EC2 上運行 Amazon Linux 2023,使用 Nginx 作為反向代理伺服器。Nginx 負責將 HTTP 流量(Port 80)轉向 HTTPS(Port 443),而 443 的流量會轉向本地端用 Go 寫的 API 服務,這個 API 服務是用 Docker 容器化部署的。
原本系統運作都很正常,直到有一天,突然收到用戶回報說 API 連不上了。檢查之後才發現,原來是 SSL 憑證過期了。照理說,我們有設定 certbot-renew.timer
來自動更新憑證,但不知道為什麼這次沒有自動更新成功,導致服務中斷。
當前環境設置
certbot 安裝與設定
我們當初是使用以下指令來產生 SSL 憑證:
sudo certbot certonly --standalone -d your.domain --non-interactive --agree-tos --no-eff-email -m your@email.com
Nginx 設定
在 /etc/nginx/conf.d/your.domain.conf
這個設定檔裡,我們是這樣設定的:
# HTTP 流量重新導向到 HTTPS
server {
listen 80;
listen [::]:80;
server_name your.domain;
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 設定
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name your.domain;
server_tokens off;
# SSL 憑證設定
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
# ... 其他設定 ...
}
啟用自動更新
設定自動更新:
sudo systemctl enable --now certbot-renew.timer
啟動 Nginx 服務
確保 Nginx 會自動啟動且立即啟動:
sudo systemctl enable --now nginx
問題診斷
症狀
- 網站存取時出現
net::err_cert_authority_invalid
錯誤 - 檢查之後發現 SSL 憑證已經過期了
排查步驟
我們是這樣一步步檢查的:
- 先看看 SSL 憑證的狀態
sudo certbot certificates
- 檢查一下
certbot-renew.timer
有沒有在運作
sudo systemctl status certbot-renew.timer
- 試試看能不能更新憑證
sudo certbot renew --dry-run
NOTE
--dry-run
只是模擬更新憑證,不會真的套用更新
錯誤原因
執行 certbot renew --dry-run
的時候,跳出這個錯誤:
Failed to renew certificate payment.dezu.group with error: Could not bind TCP port 80 because it is already in use by another process on this system (such as a web server). Please stop the program in question and then try again.
簡單來說,問題出在這裡:certbot 在 standalone 模式下需要用到 80 連接埠來做 HTTP-01 Challenge 驗證,但是這個連接埠已經被 Nginx 佔用了。
這裡要解釋一下 Let’s Encrypt 的驗證機制1:為了確保你確實可控制這個網域,Let’s Encrypt 需要進行驗證。HTTP-01 Challenge 是最簡單的驗證方式,它需要在網站的 /.well-known/acme-challenge/
路徑下放置一個驗證檔案。在 standalone 模式下,certbot 會自己啟動一個 HTTP 伺服器來提供這些驗證檔案,但這需要用 80 連接埠。當 Nginx 已經在用 80 連接埠時,certbot 就沒辦法啟動自己的 HTTP 伺服器,所以驗證就失敗了。
你會需要這樣做才能更新 SSL 憑證:
sudo systemctl stop nginx
sudo certbot renew
sudo systemctl start nginx
解決方案
我們決定把 certbot 的驗證模式從 standalone
改成 webroot
。這個模式有幾個好處:
- 不用把 Nginx 停掉
- 網站可以繼續運作
- 驗證過程比較快
在 webroot 模式下,certbot 會把驗證檔案寫到指定的目錄,然後透過 Nginx 來提供這些檔案,完成 HTTP-01 Challenge 驗證。
1. 建立 Webroot 目錄
先建立一個目錄來放驗證檔案:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo chown -R nginx:nginx /var/www/letsencrypt
2. 修改 Nginx 設定
在 Nginx 的設定檔裡加入 webroot 的路徑,讓 certbot 可以透過這個路徑來做驗證:
# HTTP 設定
server {
listen 80;
listen [::]:80;
server_name payment.dezu.group;
+ # certbot webroot 驗證路徑
+ location /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ }
location / {
return 301 https://$host$request_uri;
}
}
# ... 其他設定保持不變 ...
3. 重新載入 Nginx
先檢查設定檔有沒有問題:
sudo nginx -t
然後重新載入 Nginx:
sudo systemctl reload nginx
4. 重新產生 SSL 憑證
用 webroot 模式來產生新的憑證:
sudo certbot certonly --webroot -w /var/www/letsencrypt -d your.domain --non-interactive --agree-tos --no-eff-email -m your@email.com
如果憑證還沒過期,但是想要強制更新,可以加上 --force-renewal
這個參數:
sudo certbot certonly --webroot -w /var/www/letsencrypt -d your.domain --non-interactive --agree-tos --no-eff-email -m your@email.com --force-renewal
總結
把 certbot 的驗證模式從 standalone
改成 webroot
之後,我們就成功解決了 SSL 憑證自動更新的問題。在 webroot 模式下,certbot 會把驗證檔案寫到指定的目錄,然後透過 Nginx 來提供這些檔案,完成 HTTP-01 Challenge 驗證。這樣就不用把 Nginx 停掉,網站可以繼續運作。
建議定期檢查一下 SSL 憑證的狀態,可以用這個指令:
sudo certbot certificates
也可以看看系統日誌,監控更新過程:
sudo journalctl -u certbot.timer
Footnotes
Let’s Encrypt - Challenge Types: https://letsencrypt.org/docs/challenge-types/ ↩