1113 字
6 分鐘
解決 certbot 自動更新 SSL 憑證失敗

問題描述#

我們的系統架構是這樣的:在 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

問題診斷#

症狀#

  1. 網站存取時出現 net::err_cert_authority_invalid 錯誤
  2. 檢查之後發現 SSL 憑證已經過期了

排查步驟#

我們是這樣一步步檢查的:

  1. 先看看 SSL 憑證的狀態
sudo certbot certificates
  1. 檢查一下 certbot-renew.timer 有沒有在運作
sudo systemctl status certbot-renew.timer
  1. 試試看能不能更新憑證
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#

  1. Let’s Encrypt - Challenge Types: https://letsencrypt.org/docs/challenge-types/

解決 certbot 自動更新 SSL 憑證失敗
https://blog.bacon4dog.dev/posts/certbot_renew_failed/
作者
bacon4dog
發佈於
2025-04-02
許可協議
CC BY-NC-SA 4.0