Ubuntu 24.04 安装 Certbot 和 Nginx 插件
在 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 必须可从公共互联网访问,不能只在您的笔记本电脑上可访问。仅开放 443 的
ufw规则、云服务商安全组或 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--webroot 适用于您不希望 Certbot 接触 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 适用于没有进程监听 80 端口的情况,例如邮件服务器、只提供 443 服务的 API,或在 nginx 启动前运行的首次启动脚本。Certbot 会自行绑定 80 端口数秒。如果 nginx 正在运行,此命令会失败;请在执行期间停止 nginx:
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"这些钩子会记录在证书的续期配置中,因此自动续期时也会无人值守地执行相同的停止和启动操作。
证书存在前后都能正常工作的 server 块
这里存在先有鸡还是先有蛋的问题:如果 ssl_certificate 指向不存在的文件,nginx 会拒绝启动;而 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 永远不会收到验证请求。^~ 修饰符在其他场景中很有用:该前缀匹配后,nginx 会完全跳过正则表达式 location,因此后续添加的规则(例如 location ~ /\. { deny all; })也无法阻止验证请求。将此 location 保留在 80 端口上,即使站点其余部分已强制使用 HTTPS,证书续期仍能正常工作。
上面的两个块都从磁盘提供文件。如果 nginx 实际上位于应用前端,location / 会变为 proxy_pass 块;逐行配置反向代理 server 块介绍了该应用所需的响应头,而 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/ 符号链接都会重新指向新证书;直接使用 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 对其执行通配符展开。DNS-01 还可以解决 HTTP-01 无法处理的问题:为没有公网 80 端口的主机、内部服务、只能通过 VPS 上自托管的 WireGuard VPN 访问的服务器,以及监听私有接口的管理面板申请证书。
续期:90 天、定时器和部署钩子
Let's Encrypt 证书的有效期为 90 天。apt 软件包中的 Certbot 2.9.0 会在剩余有效期不足 30 天时续期。snap 会跟踪当前版本的 Certbot。Certbot 自 4.1 版本起,还会通过 ACME Renewal Info (ARI) 向 Let's Encrypt 请求建议的续期时间,因此可能更早续期;对于 Let's Encrypt 证书,这通常意味着在证书生命周期约经过三分之二时续期。无论哪种方式,您通常都有约 30 天的处理窗口。这样,续期故障仍是可以修复的问题,不会立即造成服务中断。预计从明年起续期会更加频繁:Let's Encrypt 的默认配置文件将在 2027 年 2 月 10 日改为签发 64 天证书,并在 2028 年 2 月 16 日改为签发 45 天证书。Let's Encrypt 不再发送到期提醒邮件,也不会有人主动提醒您,因此现在需要由您负责监控。
检查安装时提供的定时器:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew 会遍历 /etc/letsencrypt/renewal/ 中的所有配置,跳过尚未到期的配置,并使用各配置中保存的选项续期。除非您后来使用 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.shrenewal-hooks/deploy/ 中的任何可执行文件都会在续期成功后运行。--deploy-hook 标志可为单个证书执行相同操作,并将 renew_hook = ... 保存到该证书的续期配置中。certbot --nginx 会为您执行重新加载;--webroot 和 --standalone 配置不会自动执行。缺少钩子正是网站仍提供过期证书,而 certbot certificates 却正常报告新证书的原因。其他在启动时读取证书的服务也需要相同的钩子。使用 Docker 的容器化应用(例如 使用 Docker、TLS 和备份安装 Nextcloud VPS)还需要在此处配置自己的重启或重新加载步骤。
实际测试续期
sudo certbot renew --dry-run该命令会在 Let's Encrypt 的 staging 环境中执行完整的质询流程:使用相同的代码路径、防火墙和 DNS 配置,不会占用生产环境的速率限制额度(staging 使用独立且宽松得多的限制),也不会将证书保存到磁盘。如果今天测试通过,那么 60 天后的无人值守续期也会通过,前提是服务器底层环境没有发生变化。
dry run 无法证明 reload hook 会执行,因为其行为因 Certbot 版本而异。手动测试这一部分:直接执行 hook 脚本,确认 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 可访问,但令牌没有被提供。请求可能落到了其他 server block(检查哪个 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,表示该名称无法通过公网解析。可能是新记录尚未传播,也可能是记录位于注册商没有提供服务的区域中。
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,签发证书,然后恢复该 server block。
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 天后的凌晨 3 点中断,而所有人都以为证书会自动续期。
上面的 HTTP-01 路由假设您控制一台具有公网 IP 且对公网开放 80 端口的机器,也就是 VPS;DNS-01 不需要这些条件。无论使用哪种方式,上述操作步骤都相同。
相同的证书步骤也适用于使用 Apache 而不是 nginx的情况;如果无法使用公有证书,在 Ubuntu 上配置自签名证书即可满足内部服务的需求。
FAQ
如果网站仅提供 HTTPS,是否需要开放端口 80?
需要,因为 HTTP-01 验证需要端口 80。Let's Encrypt 始终从端口 80 发起验证请求,而 Certbot 没有 TLS-ALPN-01 实现。因此,防火墙仅开放 443 会同时阻止首次签发和之后的每次无人值守续期。将端口 80 重定向到 HTTPS 没问题,验证请求会遵循重定向。完全跳过端口 80 的唯一方法是使用 DNS-01,可以通过提供商插件实现,也可以使用 Certbot 的 --manual --preferred-challenges dns 实现。后者需要您手动添加 TXT 记录;除非提供 --manual-auth-hook 脚本,否则续期将不再自动执行。
在 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 将其作为 glob 展开。
续期成功后,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。