SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

Ubuntu 24.04 安裝 Certbot 與 Nginx 設定教學

在 Ubuntu 24.04 使用 apt 或 snap 安裝 Certbot 並取得 Let's Encrypt 憑證。本文說明 certbot --nginx 指令、apt 與 snap 的差異,以及解決 port 80 驗證超時失敗的關鍵設定。

安裝 Certbot:apt 或 snap

在 Ubuntu 24.04 上,sudo apt install certbot python3-certbot-nginx 可提供可正常運作的 Certbot,用來簽發受公開信任的 Let's Encrypt 憑證。Certbot 上游文件改為建議使用 snap;兩者差異不大。snap 會追蹤上游版本,封存套件則追蹤 LTS 隨附的版本,並取得安全性修正。

請選擇其中一種。安裝兩份 Certbot 會讓兩個續期計時器指向同一個 /etc/letsencrypt 樹狀目錄,而你忘記的那一份最終會造成意外。

apt 方式:

sudo apt update
sudo apt install certbot python3-certbot-nginx

這會安裝 /usr/bin/certbot、nginx 外掛、certbot.service + certbot.timer 配對,以及在 systemd 下不執行任何操作的 /etc/cron.d/certbot 項目。

snap 方式:

sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

snap 會附帶自己的計時器 snap.certbot.renew.timer。安裝 snap 之前,請先移除 apt 套件。

完成後,兩種安裝方式的行為相同,只有決定續期的確切時間不同(詳見下方的 Renewal)。Certbot 2.x 預設使用 ECDSA(P-256)金鑰。只有在用戶端無法使用 ECDSA 時,才傳入 --key-type rsa。所有狀態都位於 /etc/letsencrypt 下:archive/ 存放實際的金鑰與憑證檔案,live/ 連結至目前使用的檔案,renewal/ 為每張憑證存放一個設定檔,accounts/ 則是你的 ACME 帳戶金鑰。

HTTP-01 實際執行的內容,以及為何無法省略 80 埠

HTTP-01 challenge 是一個 callback。您向 Let's Encrypt 申請涵蓋 example.com 的憑證;Let's Encrypt 會在公開 DNS 查詢該名稱,連線至查到的位址上的 80 埠,並請求 http://example.com/.well-known/acme-challenge/<token>。您的伺服器會回應 Certbot 剛寫入磁碟的完全相同 token 內容。這就是完整機制。由此會產生 3 個結果,也涵蓋了大多數簽發失敗的原因。

  • 80 埠必須能從公開網際網路連線,不能只允許您的筆電連線。只開放 443 的 ufw 規則、雲端供應商的 security group,或 VPS 主控台防火牆,都會導致簽發失敗,也會使日後的續期失敗。
  • DNS 必須已指向這台伺服器。 驗證伺服器會從外部自行查詢 DNS;您的 /etc/hosts 設定與瀏覽器快取對它沒有作用。
  • 如果您發布 AAAA 記錄,系統會優先嘗試 IPv6。 只有在 IPv6 連線逾時時,Let's Encrypt 才會改用 IPv4 重試。遭拒絕的 IPv6 連線不會重試;如果過時的 AAAA 記錄指向一台會接受連線、但提供其他內容的主機,驗證就會直接失敗。

系統允許重新導向:驗證會依循 HTTP 重新導向至 HTTPS,也不在意對端的憑證不存在、已過期或是自簽憑證。但驗證不會從 80 埠以外的地方開始。Certbot 沒有 TLS-ALPN-01 實作,因此「只使用 443」並不能避開這項要求。

選擇驗證器:--nginx、--webroot、--standalone

若 nginx 已在執行且負責處理該網域,--nginx 是預設的最佳選擇。Certbot 會解析您的設定檔、注入暫時的驗證路徑、重新載入 nginx、完成驗證,最後將 TLS 指令寫入您的 server block。此過程無須停機。

sudo certbot --nginx -d example.com -d www.example.com

適用於全新伺服器的腳本化操作:

sudo certbot --nginx \
  -d example.com -d www.example.com \
  --agree-tos -m ops@example.com --no-eff-email \
  --redirect --non-interactive

若您不希望 Certbot 更動 nginx 設定檔(例如設定檔是透過模板產生、由 git 管理或透過 Ansible 推送),則 --webroot 最為合適。Certbot 僅會將驗證檔案寫入您現有的服務目錄中。

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

若目前沒有任何服務監聽 80 埠(例如郵件伺服器、僅使用 443 埠的 API,或是在安裝 nginx 前執行的初始化腳本),則 --standalone 是正確選擇。Certbot 會自行綁定 80 埠數秒以完成驗證。若 nginx 正在執行,此指令會失敗,請在執行前後停止服務:

sudo certbot certonly --standalone -d mail.example.com \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx"

這些掛鉤(hooks)會記錄在憑證的續期設定中,因此在自動續期時,系統會自動執行相同的停止與啟動程序。

憑證檔案存在前後都能運作的 server block

這是先有雞還是先有蛋的問題:nginx 會拒絕啟動,因為 ssl_certificate 指向的檔案不存在;但 Certbot 又無法在 nginx 停止時完成驗證。先讓網站透過連接埠 80 提供服務。

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
        try_files $uri =404;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

執行 sudo nginx -t && sudo systemctl reload nginx,確認從主機外部可以取得 curl -I http://example.com/ 的回應,然後再簽發憑證。完成後:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

較長的 ACME 前綴已優先匹配 location /,因此 return 301 永遠不會收到 challenge 請求。^~ 修飾詞在其他情境很有用:該前綴一旦匹配,nginx 就會略過 regex location,因此之後新增的規則(例如 location ~ /\. { deny all; })不會阻擋 challenge。將這個 location 保留在連接埠 80 上,即使網站其餘部分改為只提供 HTTPS,續期仍能正常運作。

上方的兩個 block 都是從磁碟提供檔案;如果 nginx 改為位於應用程式前端,location / 會改成 proxy_pass block,而 逐行說明反向代理 server block 會涵蓋該應用程式所需的標頭;ACME location 與 TLS 指令則維持不變。

HTTP/2 語法取決於 nginx 版本。Ubuntu 24.04 提供 nginx 1.24,要求使用內嵌寫法 listen 443 ssl http2;;若使用獨立的 http2 on; 指令,nginx 會啟動失敗。Debian 13 提供 nginx 1.26,要求使用 http2 on;,對內嵌寫法只會發出已棄用的警告。先檢查 nginx -v。

讓 nginx 指向 live/,不要指向 archive/。每次續期時,live/ symlink 都會重新指向新檔案;若使用指向 archive/ 的固定路徑,服務就會綁定在即將過期的憑證上。

萬用字元憑證需使用 DNS-01,而 DNS-01 需搭配外掛程式

萬用字元憑證 (*.example.com) 無法透過 HTTP-01 進行驗證,因為沒有單一主機名稱可供擷取檔案。DNS-01 是唯一途徑:您必須透過發布 _acme-challenge.example.com TXT 記錄來證明控制權。Certbot 需要您 DNS 提供商的 API 憑證才能自動執行此操作,這正是提供商外掛程式存在的目的。萬用字元憑證完整操作指南 涵蓋了 TXT 記錄的運作機制以及手動模式下的續期陷阱;以下為 Cloudflare 的簡要版本。

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

在 apt 路徑上,該套件名稱為 sudo apt install python3-certbot-dns-cloudflare。憑證應存放於僅限 root 存取的檔案中:

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

請將 Token 的權限限制在該區域的 DNS 編輯權限。這是您 DNS 的金鑰;請務必妥善保管。

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

請將萬用字元加上引號,以避免 shell 進行 glob 展開。DNS-01 同時解決了 HTTP-01 無法處理的問題:例如沒有公開 80 埠的主機憑證、內部服務、僅能透過 VPS 上的自架 WireGuard VPN 存取的機器,或是私有介面上的管理面板。

續期:90 天、計時器與部署掛鉤

Let’s Encrypt 憑證的有效期限為 90 天。apt 套件(Certbot 2.9.0)會在剩餘天數少於 30 天時續期。snap 會追蹤目前版本的 Certbot;自版本 4.1 起,Certbot 也會透過 ACME Renewal Info(ARI)向 Let's Encrypt 詢問建議的續期時間,因此可能會更早續期。對 Let's Encrypt 憑證而言,這通常代表在有效期限經過約三分之二時續期。無論採用哪種方式,通常都會有約 30 天的處理時間,讓失敗的續期成為可以修正的問題,而不是服務中斷。從明年起,續期可能會更頻繁:Let's Encrypt 的預設設定檔將於 February 10, 2027 改用 64 天憑證,並於 February 16, 2028 改用 45 天憑證。Let's Encrypt 已不再寄送到期警告電子郵件,也不會有人提醒你,因此現在必須自行監控。

檢查安裝時提供的計時器:

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew 會逐一檢查 /etc/letsencrypt/renewal/ 中的所有設定,略過尚未到期的項目,並使用該設定中儲存的選項續期。這些選項是首次執行時使用的 flags,除非你之後透過 certbot reconfigure 變更它們。這就是首次執行很重要的原因:系統會記錄那次執行所使用的設定。

更新磁碟上的憑證檔案本身不會產生任何作用。nginx 會繼續從記憶體提供舊憑證,直到收到重新載入的指示。請設定一次部署掛鉤:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

renewal-hooks/deploy/ 中任何具備執行權限的檔案,都會在任何一次成功續期後執行。--deploy-hook flag 對單一憑證具有相同作用,並會將 renew_hook = ... 儲存到該憑證的續期設定中。certbot --nginx 會代你重新載入;--webroot 和 --standalone 設定則不會。缺少掛鉤正是網站仍提供過期憑證,而 certbot certificates 卻正常回報新憑證的原因。任何在啟動時讀取憑證的其他服務,也需要相同的掛鉤;容器化應用程式,例如 使用 Docker、TLS 與備份的 Nextcloud VPS 安裝,也需要在這裡設定自己的重新啟動或重新載入步驟。

實際測試續期

sudo certbot renew --dry-run

此命令會在 Let's Encrypt 的 staging 環境中執行完整的驗證流程:使用相同的程式碼路徑、防火牆與 DNS,且不會計入 production 的速率限制(staging 有自己的限制,寬鬆許多),也不會將憑證儲存到磁碟。如果今天測試通過,在伺服器底層設定沒有變更的前提下,60 天後的無人值守續期也會通過。

dry run 無法證明 reload hook 會執行;其行為會因 Certbot 版本而異。請手動測試這部分:直接執行 hook script,確認 systemctl reload nginx 成功,並檢查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf。

實際會遇到的錯誤

Could not bind to IPv4 or IPv6.,--standalone,因為 nginx 已經占用連接埠 80。使用 --nginx 或 --webroot,或在執行期間暫停 nginx。使用 sudo ss -lntp | grep ':80' 確認目前占用該連接埠的程序。

Timeout during connect (likely firewall problem),Let's Encrypt 無法連線至 80 埠。請由外向內逐項檢查:sudo ufw status(使用 sudo ufw allow 'Nginx Full' 開放),接著檢查 VPS 供應商自己的防火牆,最後檢查 DNS。請從不是該伺服器的其他位置進行測試:curl -sSv http://example.com/.well-known/acme-challenge/test。過期的 AAAA 記錄也會產生相同訊息。

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404,連接埠 80 可連線,但未提供 token。請求落入了其他 server block(確認哪個區塊負責 default_server),或傳給 -w 的目錄不是 nginx 實際提供服務的目錄。在 /var/www/example.com/.well-known/acme-challenge/test 建立檔案,並從外部擷取該檔案;如果回應 404,問題就不在憑證。

DNS problem: NXDOMAIN looking up A for example.com,該名稱無法在公開 DNS 中解析。可能是新建記錄尚未完成傳播,或該記錄位於註冊商未提供服務的 DNS 區域中。

too many certificates (5) already issued ...、速率限制,以及除錯時反覆執行而觸發的限制。訊息會指出受限項目,並以 retry after 時間結尾。Let’s Encrypt 每週最多簽發 5 張名稱集合完全相同的重複憑證,且不提供此限制的豁免,因此只能等待限制解除。另一方面,每個註冊網域每週可申請 50 張新憑證;如需提高此上限,可以申請豁免。若申請的名稱集合與現有憑證完全相同,會視為續期,不受此限制影響;透過 ARI 協調的續期則不受任何限制影響。使用 --dry-run 對 staging 環境進行除錯。

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory,nginx 設定了從未簽發的憑證,或該憑證已使用 certbot delete 移除。先將 TLS server block 註解掉並啟動 nginx,簽發憑證後再還原該區塊。

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed,該檔案會隨 nginx 外掛套件一併安裝。在未安裝 python3-certbot-nginx 的 certonly 主機上,請新增該外掛,或將 include 行替換為自訂的 ssl_protocols 與 ssl_ciphers 設定。

大規模環境中的管理

單張憑證最多可包含 100 個名稱,使用單一 certbot --nginx -d a.example.com -d b.example.com ... 看似方便,但只要有一筆過期的 DNS 記錄驗證失敗,就會連帶使該憑證上的所有其他名稱失效。為每個網站分開建立憑證,網站之間的失效會彼此獨立;對於一台託管多個服務的伺服器,這才是理想做法。網站數量超過少數幾個後,支援 ACME 的入口代理就能發揮價值:例如 使用 Docker Compose 執行多個應用程式的 Traefik 反向代理,會自行申請並續期憑證,完全不需要 Certbot。入口代理的選擇是另一項決策,而 比較 Nginx、Caddy 與 Traefik 時,主要取決於你希望代理替你處理多少憑證與各應用程式的設定工作。

完整備份 /etc/letsencrypt,包括 sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt,並保留符號連結。該目錄樹包含 accounts/,也就是 ACME 帳戶金鑰;這把金鑰無法重新產生出完全相同的版本。搬遷到新的 VPS 時,只需使用 -a 同步該目錄樹、安裝 Certbot、重新指向 DNS,並在切換前執行 certbot renew --dry-run。

重建伺服器或移轉到新的 LTS 版本時,續期計時器不會隨你一併移轉。完成任何搬遷、快照還原或發行版升級後,執行 systemctl list-timers 'certbot*',並進行一次 --dry-run。略過這項檢查,網站可能會在 89 天後凌晨 3am 突然中斷,因為所有人都以為憑證會自動續期。

上述 HTTP-01 路由假設你能控制該機器,且它具有公開 IP,並向整個網際網路開放 port 80;換句話說,就是 VPS。DNS-01 不需要這些條件。上述操作方式適用於這兩種情境。

相同的憑證步驟也適用於 使用 Apache 而非 nginx 的情況;如果無法使用公開憑證,則可透過 在 Ubuntu 上建立自簽憑證 支援內部服務。

FAQ

如果網站只提供 HTTPS,還需要開放 80 埠嗎?

需要,因為 HTTP-01 challenge 需要使用 80 埠。Let’s Encrypt 一律從 80 埠開始驗證請求,而 Certbot 沒有 TLS-ALPN-01 實作。因此,僅開放 443 的防火牆會阻擋首次簽發,以及之後每次無人值守的續期。將 80 埠重新導向 HTTPS 沒有問題,驗證程序會跟隨重新導向。完全略過 80 埠的唯一方式是使用 DNS-01,可搭配 provider plugin,或使用 Certbot 的 --manual --preferred-challenges dns。在後者中,您必須自行新增 TXT 記錄;除非提供 --manual-auth-hook script,否則續期不會自動執行。

在 Ubuntu 24.04 上為 nginx 安裝 Certbot,應使用 apt 還是 snap?

使用 apt。sudo apt install certbot python3-certbot-nginx 會在 Ubuntu 24.04 上提供 Certbot 2.9.0,版本已足以支援本指南的所有內容,並透過 unattended-upgrades 取得安全性修補程式,也不需要 snapd。只有在您需要立即取得最新版本,或需要只能以 snap 發布的 DNS plugin 時,才選擇 snap。無論採用哪一種方式,都只能選一個:安裝兩份會讓兩個續期 timer 指向同一個 /etc/letsencrypt tree,而被遺忘的那一份最終會造成問題。

Certbot 能為 nginx 簽發 wildcard 憑證嗎?

只能透過 DNS-01。像 *.example.com 這樣的 wildcard 沒有可供擷取 challenge file 的單一 hostname,因此 --nginx、--webroot 和 --standalone 都無法使用。請安裝 DNS provider 的 plugin,將具有限定權限的 API token 存放在僅允許 root 存取的 credentials file 中,然後執行 certbot certonly --dns-cloudflare -d example.com -d '*.example.com',並以引號包住 wildcard,避免 shell 將其視為 glob pattern。

為什麼 nginx 在續期成功後仍提供舊憑證?

nginx 會將憑證保留在記憶體中,除非重新載入,否則不會注意到磁碟上的新檔案。certbot --nginx 會自動重新載入,但 --webroot 和 --standalone runs 不會,因此續期可能已成功,而瀏覽器仍會看到即將到期的憑證。在 /etc/letsencrypt/renewal-hooks/deploy/ 中放入可執行的 script,讓它執行 nginx -t && systemctl reload nginx;每次續期成功後都會觸發該 script。

certbot renew --dry-run 能證明續期會正常運作嗎?

大致可以。它會在 staging environment 中,使用相同的防火牆、相同的 DNS 與相同的 code path,執行實際的 challenge,不會影響 production rate limits,也不會將任何憑證儲存到磁碟。因此,測試通過表示網路部分正常。不過,它無法可靠證明 deploy hook 會觸發。請另外測試:手動執行 hook script,並檢查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf。