Ubuntu 24.04 安装 Certbot 配置 Nginx HTTPS
在 Ubuntu 24.04 上选择 apt 或 snap 安装 Certbot,执行 certbot --nginx 签发 Let's Encrypt 证书,并排查端口 80、DNS、IPv6 与续期超时问题。
安装 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/certbotsnap 会安装自己的计时器 snap.certbot.renew.timer。安装 snap 之前,先删除 apt 软件包。
之后,两种安装方式的行为相同。Certbot 2.x 默认使用 ECDSA(P-256)密钥。只有在客户端不支持 ECDSA 时,才传递 --key-type rsa。所有状态都存放在 /etc/letsencrypt 下:archive/ 保存实际的密钥和证书文件,live/ 链接到当前文件,renewal/ 为每个证书保存一个配置文件,accounts/ 保存您的 ACME 帐户密钥。
HTTP-01 实际执行的操作,以及为什么端口 80 不是可选项
HTTP-01 挑战是一种回调机制。您向 Let's Encrypt 请求覆盖 example.com 的证书;Let's Encrypt 会通过公共 DNS 解析该域名,在解析到的地址上连接 端口 80,然后请求 http://example.com/.well-known/acme-challenge/<token>。您的服务器返回 Certbot 刚写入磁盘的确切令牌内容。整个机制就是这样。由此会产生 3 个后果,它们导致了大多数签发失败。
- 端口 80 必须可从公网访问,不能只允许您的笔记本电脑访问。
ufw规则、云服务商安全组或 VPS 控制台防火墙如果只开放 443,就会导致本次签发失败,后续续期也会失败。 - DNS 必须已经指向这台服务器。 验证服务器会从外部自行查询 DNS;您的
/etc/hosts记录和浏览器缓存对它不起作用。 - 如果发布了 AAAA 记录,系统会优先尝试 IPv6。 如果 IPv6 连接直接失败,Let's Encrypt 会改用 IPv4 重试;但如果过期的 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--webroot 适用于您不希望 Certbot 接触 nginx 配置的情况。例如,nginx 配置由模板生成、保存在 git 中,或通过 Ansible 下发。Certbot 只会将验证文件写入您已经提供服务的目录。
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"--standalone 适用于没有任何服务监听 port 80 的情况,例如邮件服务器、只提供 443 服务的 API,或在 nginx 创建前运行的首次启动脚本。Certbot 会自行绑定 port 80 几秒钟。如果 nginx 正在运行,此命令会失败。请在执行期间停止 nginx:
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"这些 hook 会记录在证书的续期配置中,因此续期时也会自动执行相同的停止和启动操作。
证书文件存在前后都能正常工作的 server 块
这里存在先有鸡还是先有蛋的问题:如果 ssl_certificate 指向不存在的文件,nginx 会拒绝启动;而 nginx 停止运行时,Certbot 又无法完成验证。先让网站通过 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 块吞掉验证请求。将该 location 保留在 80 端口上,可确保网站其余部分仅使用 HTTPS 后,证书续期仍能正常进行。
上面的两个块都从磁盘提供文件;如果 nginx 实际上位于应用前端,location / 应改为 proxy_pass 块。反向代理 server 块逐行说明介绍了应用所需的响应头,而 ACME location 和 TLS 指令保持不变。
HTTP/2 语法取决于 nginx 版本。混用两种语法会导致启动错误。Ubuntu 24.04 提供 nginx 1.24,要求使用内联写法:listen 443 ssl http2;。Debian 13 提供更新版本的 nginx,要求使用独立的 http2 on; 指令。先检查 nginx -v。
让 nginx 指向 live/,不要指向 archive/。每次续期时,live/ 符号链接都会重新指向新证书;如果直接使用 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将令牌权限限制为仅修改该区域 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 天。剩余有效期少于 30 天时,Certbot 会执行续期。这样,您有 30 天时间处理续期故障,问题通常只是需要修复的异常,而不是服务中断。Let's Encrypt 已不再发送到期提醒邮件,也不会有人提醒您,因此现在需要由您负责监控。
检查安装时附带的计时器:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew 会遍历 /etc/letsencrypt/renewal/ 中的所有配置,跳过剩余有效期超过 30 天的证书,并完全使用首次运行时的参数续期其余证书。因此,首次运行很重要,因为系统会记录这次运行使用的参数。
更新磁盘上的证书文件本身不会产生任何效果。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.shrenewal-hooks/deploy/ 中任何具有执行权限的文件,都会在续期成功后运行。--deploy-hook 标志对单个证书执行相同操作,并将 renew_hook = ... 写入其续期配置。certbot --nginx 会为您执行重新加载;--webroot 和 --standalone 配置不会自动重新加载。缺少钩子正是网站仍提供过期证书,而 certbot certificates 却正常报告新证书的原因。任何在启动时读取证书的其他服务也需要使用相同的钩子;例如,使用 Docker、TLS 和备份的 Nextcloud VPS 安装 还需要在此处配置自身的重启或重新加载步骤。
实际测试续期
sudo certbot renew --dry-run该命令会针对 Let's Encrypt 的预发布环境执行完整验证流程:使用相同的代码路径、防火墙和 DNS,不消耗速率限制额度,也不会向磁盘写入任何内容。如果今天测试通过,那么 60 days 后的无人值守续期也会通过,前提是服务器底层配置没有变化。
dry run 无法证明 reload hook 会执行,因为不同 Certbot 版本的行为可能不同。手动测试这一部分:直接执行 hook 脚本,确认 systemctl reload nginx 成功,并检查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf。
The errors you will actually hit
Could not bind to IPv4 or IPv6., --standalone while nginx already holds port 80. Use --nginx or --webroot, or stop nginx around the run. Confirm the holder with sudo ss -lntp | grep ':80'.
Timeout during connect (likely firewall problem), Let's Encrypt could not reach port 80. Walk outward: sudo ufw status (open it with sudo ufw allow 'Nginx Full'), then the VPS provider's own firewall, then DNS. Test from somewhere that is not your server: curl -sSv http://example.com/.well-known/acme-challenge/test. A stale AAAA record produces this same message.
unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, port 80 is reachable, but the token is not being served. The request landed in a different server block (check which one owns default_server), or the directory passed to -w is not the one nginx serves. Drop a file at /var/www/example.com/.well-known/acme-challenge/test and fetch it externally; if that 404s, the certificate was never the problem.
DNS problem: NXDOMAIN looking up A for example.com, the name does not resolve publicly. New records that have not propagated, or a record in a zone your registrar is not serving.
too many certificates already issued for: example.com, a rate limit, and the one people hit while debugging in a loop. Let's Encrypt caps duplicate certificates, the same exact set of names, at five per week, and separately allows 50 new certificates per registered domain per week; nothing unblocks either except time. Debug against staging with --dry-run.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx is configured for a certificate never issued, or one removed with certbot delete. Comment out the TLS server block, start nginx, issue, restore the block.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, that file arrives with the nginx plugin package. On a certonly box without python3-certbot-nginx, either add the plugin or replace the include line with your own ssl_protocols and ssl_ciphers settings.
规模化管理
单个证书最多可包含 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 天后的凌晨 3 点中断,而所有人都以为证书会自动续期。
以上操作都假设您控制这台机器,并且它拥有公网 IP,端口 80 对公网开放;换句话说,这是一台 VPS。上述操作流程适用于所有这类环境。
在 Apache 而不是 nginx 上,证书操作步骤也同样适用,参见在 Apache 上而不是 nginx 上配置;如果无法使用公共证书,则可通过在 Ubuntu 上配置自签名证书保护内部服务。
FAQ
如果网站只提供 HTTPS,还需要开放 80 端口吗?
需要,因为 HTTP-01 挑战需要使用该端口。Let's Encrypt 始终从 80 端口开始发送验证请求,而 Certbot 没有 TLS-ALPN-01 实现。因此,只开放 443 的防火墙会阻止首次签发,以及之后每次无人值守续期。将 80 端口重定向到 HTTPS 没有问题,验证请求会遵循该重定向。完全不使用 80 端口的唯一方法是使用带有提供商插件的 DNS-01。
在 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 插件时,才选择 snap。无论选择哪种方式,都必须只安装其中一种:两种安装会针对同一个 /etc/letsencrypt 目录树创建两个续期计时器,而被遗忘的那个才会导致问题。
Certbot 能为 nginx 签发通配符证书吗?
只能通过 DNS-01 签发。像 *.example.com 这样的通配符没有可用于获取挑战文件的单个主机名,因此 --nginx、--webroot 和 --standalone 都无法使用。安装 DNS 提供商对应的插件,将范围受限的 API 令牌存放在仅允许 root 访问的凭据文件中,然后运行 certbot certonly --dns-cloudflare -d example.com -d '*.example.com',并为通配符加引号,以防 shell 将其作为通配模式展开。
续期成功后,nginx 为什么仍然提供旧证书?
nginx 会将证书保存在内存中。在重新加载之前,它不会发现磁盘上的新文件。certbot --nginx 会自动执行重新加载,但 --webroot 和 --standalone 不会,因此续期可能已经成功,而浏览器仍然看到即将过期的证书。将一个可执行脚本放入 /etc/letsencrypt/renewal-hooks/deploy/,让脚本运行 nginx -t && systemctl reload nginx。这样,每次续期成功后都会执行该脚本。
certbot renew --dry-run 能证明续期一定会成功吗?
大体上可以。它会在 staging 环境中执行真实的挑战流程,使用相同的防火墙、相同的 DNS 和相同的代码路径,不消耗速率限制额度,也不会向磁盘写入内容。因此,测试通过表示网络部分运行正常。但它不能可靠证明部署钩子会执行。请单独测试部署钩子:手动运行钩子脚本,然后检查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf。