SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 更新于 2026-07-19

Ubuntu 24.04:Certbot + Let's Encrypt + nginx

在 Ubuntu 24.04 上通过 apt 或 snap 安装 Certbot,为 nginx 申请 Let's Encrypt 证书,并解决线上服务器上导致 HTTP-01 续期失败的 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 软件包。

装好之后两者行为一致。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 的证书;它会在公共 DNS 中解析该域名,向解析到的地址上的 80 端口 发起连接,请求 http://example.com/.well-known/acme-challenge/<token>。您的服务器则以 Certbot 刚刚写入磁盘的确切 token 内容作答。整个机制就是这样。由此引出三条推论,它们解释了大多数签发失败的原因。

  • 80 端口必须能从公网访问,而不只是从您的笔记本能访问。一条 ufw 规则、云服务商的安全组,或只开放 443 的 VPS 控制台防火墙,都会让签发失败,并连带影响之后每一次续期。
  • 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 会解析您的配置,注入一个临时的验证 location,重载 nginx,完成验证,然后把 TLS 指令写入您的 server 块。全程没有停机。

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 正在 运行,这会失败,请在运行前后把它停掉:

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,续期依然能正常工作。

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

把 token 的权限范围限定为对那一个 zone 的 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 天、定时器与 deploy hook

Let's Encrypt 证书的有效期为 90 天。Certbot 会在剩余不足 30 天时续期,这给了您一个 30 天的窗口,在这段时间里,续期出问题只是个能修复的麻烦,而不是一次服务中断。Let's Encrypt 不再发送到期提醒邮件,没有人会来拍您肩膀提醒您,所以监控现在得由您自己负责。

检查您安装时自带的定时器:

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

certbot renew 会遍历 /etc/letsencrypt/renewal/ 中的每个配置,跳过不在 30 天窗口内的证书,并用与初次运行完全相同的参数续期其余证书。这就是为什么第一次运行如此重要:它就是被记录下来的那一次。

仅仅续期磁盘上的文件本身不会带来任何改变:nginx 会一直从内存里提供旧证书,直到有东西通知它重载。把 deploy hook 配置一次即可:

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 参数对单张证书做同样的事,会在其续期配置中存入 renew_hook = ...certbot --nginx 会替您重载;而 --webroot--standalone 的方式不会。缺少这个 hook,正是为什么一个站点在 certbot certificates 兴高采烈地报告证书是新的时,却仍在提供一张过期证书。任何其他在启动时读取证书的程序都需要同样的 hook,比如像 使用 Docker、TLS 和备份的 Nextcloud VPS 安装 这样的容器化应用,也需要在这里接上它自己的重启或重载步骤。

真正测试续期

sudo certbot renew --dry-run

这会对 Let's Encrypt 的 staging 环境执行完整的验证:相同的代码路径、相同的防火墙、相同的 DNS,不消耗速率限制额度,也不往磁盘写入任何东西。如果今天能通过,那么 60 天后的自动续期也会通过,前提是这台机器底层没有发生变化。

dry run 并不能证明您的重载 hook 会触发,这方面的行为因 Certbot 版本而异。请手动测试这一半:直接执行 hook 脚本,确认 systemctl reload nginx 成功,并检查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf

您真正会遇到的错误

Could not bind to IPv4 or IPv6. — 在 nginx 已经占用 80 端口时使用了 --standalone。改用 --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 块里(检查是哪一个拥有 default_server),或者传给 -w 的目录并不是 nginx 实际提供服务的那个。在 /var/www/example.com/.well-known/acme-challenge/test 放一个文件,然后从外部获取它;如果这也返回 404,那么问题从来就不在证书上。

DNS problem: NXDOMAIN looking up A for example.com — 该域名在公网无法解析。可能是新记录尚未传播,或者记录所在的 zone 并未由您的注册商实际提供服务。

too many certificates already issued for: example.com — 一个速率限制,也是人们反复调试时最常撞上的那个。Let's Encrypt 把重复证书(即完全相同的一组域名)限制为每周五张,另外允许每个注册域名每周签发 50 张新证书;除了等待,没有别的办法解除。请用 --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 块注释掉,启动 nginx,签发证书,再恢复该块。

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed — 这个文件随 nginx 插件软件包一起安装。在一台只用 certonly、没装 python3-certbot-nginx 的机器上,要么补装该插件,要么把 include 那一行替换成您自己的 ssl_protocolsssl_ciphers 设置。

规模化地掌控这一切

一张证书最多可以承载 100 个域名,用一条 certbot --nginx -d a.example.com -d b.example.com ... 全部搞定很有诱惑力,直到某一条过期的 DNS 记录验证失败,把这张证书上的其他所有域名一起拖垮。为每个站点分开签发证书,它们就会各自独立地失败,这正是您在一台托管多项服务的机器上想要的。当站点超过几个之后,一个懂 ACME 的入口就物有所值了:在 Docker Compose 下运行多个应用的 Traefik 反向代理 会自己申请并续期证书,Certbot 就彻底退出了舞台。

整体备份 /etc/letsencrypt,用 sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt,保持符号链接完好。这个目录树里有 accounts/,也就是您的 ACME 账户密钥,它无法被一模一样地重新生成。这样一来,迁移到新的 VPS 就简化为:用 -a 参数 rsync 整个目录树、安装 Certbot、重新指向 DNS,并在正式切换前运行 certbot renew --dry-run

重建机器或迁移到新的 LTS 时,续期定时器并不会跟着走。在任何迁移、快照恢复或发行版升级之后,运行 systemctl list-timers 'certbot*' 和一次 --dry-run。省掉这一步,就是为什么一个站点会在 89 天后的凌晨 3 点悄然下线,而所有人都以为那张证书在自动续期。

这一切都假设您掌控着一台机器,它有公网 IP,并向外界开放了 80 端口,换句话说就是一台 VPS。上面这些机制在任何一台 VPS 上都是一样的。

同样的证书步骤也适用于 使用 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。在 Ubuntu 24.04 上,sudo apt install certbot python3-certbot-nginx 会给您 Certbot 2.9.0,对本指南中的一切来说都足够新,能通过 unattended-upgrades 获得安全补丁,而且不需要 snapd。只有当您必须立刻用上最新版本,或需要某个只以 snap 形式分发的 DNS 插件时,才选 snap。无论哪种方式,都只选一个:装两份就意味着两个续期定时器指向同一个 /etc/letsencrypt 目录树,而被您忘掉的那个正是会咬您一口的那个。

Certbot 能为 nginx 签发通配符证书吗?

只能通过 DNS-01。像 *.example.com 这样的通配符没有一个具体的主机名可供获取验证文件,所以 --nginx--webroot--standalone 都行不通。安装您 DNS 服务商的插件,把一个限定权限的 API token 放进一个仅 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、相同的代码路径),不消耗速率限制额度,也不往磁盘写入任何东西,所以通过就意味着网络这一半是可靠的。不过它并不能可靠地证明您的 deploy hook 会触发。请单独测试这一点:手动运行 hook 脚本,并检查 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf