🔒 第02章 · Let's Encrypt 签发与续期

certbot nginx/webroot 自动续期

02 | Let's Encrypt 签发与续期

上一章讲了证书和信任链,但真正让人头疼的是"证书从哪来、到期怎么办"。这一章用 certbot 把这事自动化:免费签发、自动续期、出了故障怎么自查。文中所有命令和路径都取自本机真实环境——bangdream.xyz、dsh.bangdream.xyz、gitlab.bangdream.xyz、lovelive.top 四张证书就是靠这套流程跑起来的。

Let's Encrypt 是什么

Let's Encrypt 是一个免费的、自动化的、开放的证书颁发机构(CA)。它给域名签发证书,通过 ACME 协议证明"你确实控制这个域名",然后签发一张 90 天有效的证书。

为什么是 90 天?因为短有效期 + 自动续期 = 安全。万一私钥泄漏,90 天后自动换新,泄漏面可控;也倒逼大家把续期自动化,避免"证书忘续导致服务瘫痪"。

安全提示:看到"证书要钱、30/60/90 天、还只给半年"的商业 CA 时别慌。Let's Encrypt 免费且自动,90 天是设计使然,不是缺陷——关键在于把续期做成自动化,而不是靠人记得。

certbot 安装

certbot 是 Let's Encrypt 官方推荐的客户端,可插拔的 authenticator(怎么验证域名)和 installer(怎么装证书)。

# Debian/Ubuntu
apt install -y certbot python3-certbot-nginx

# 查看本机版本(本机为 2.9.0)
certbot --version

ACME 验证方式:证明"域名是你的"

让 CA 相信你拥有域名,靠的是在域名可控的位置展示一个令牌。certbot 内置三种主要验证:

验证方式 原理 适用场景 本机用的是什么
HTTP-01 在 /.well-known/acme-challenge/ 放一个令牌文件,CA 用 HTTP 访问你的 80 端口验证 有 80 端口的普通站点 否(本机用 nginx)
DNS-01 在 _acme-challenge 的 DNS TXT 记录里放凭证 通配符 *.domain 或纯 API 场景 否
TLS-ALPN-01 在 443 端口的 handshake 里放一个特殊 ALPN 扩展 需要 443 又不想占 80 的场景 否

certbot 会和 authenticator 插件配合去落地这些文件。本机走的是 nginx 认证器——它自动操作 nginx 配置文件完成 HTTP-01。

用 nginx 认证器签发(本机真实做法)

# certbot 会自动读 nginx 配置、生成临时验证文件、申请并安装证书
certbot --nginx -d bangdream.xyz -d www.bangdream.xyz

# 一次性签多个域名,每个 -d 都进 SAN(上面的清单可叠加)
certbot --nginx -d gitlab.bangdream.xyz -d dsh.bangdream.xyz

-d 可以重复。一个 certbot 命令签多个域名,最终是一张 SAN 证书,而不是每个域名一张。你看到的 fullchain.pem 里 SAN 会是 DNS:bangdream.xyz, DNS:www.bangdream.xyz——这就是一个证书盖多个活。

certbot 装完之后对配置做了什么

签发成功,nginx installer 会自动往你的 server 块里塞四条 TLS 配置(本机 bangdream.xyz.conf 里可见):

# managed by Certbot —— 这几行是 certbot 自动写入的
ssl_certificate     /etc/letsencrypt/live/bangdream.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bangdream.xyz/privkey.pem;
include             /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

同时它把一个 80 端口的 server 块改成 301 跳转到 HTTPS,并在文件里留下 # managed by Certbot 标记。

易错点:不要删掉 # managed by Certbot 标记,也不要手动和它抢着改这些行。 certbot 靠这些注释来判断"这段是我管的",你改动后下一轮续期会报 An unexpected error occurred。想自定义 TLS 参数,去改 options-ssl-nginx.conf 或加自己的 server 块,别动 certbot 管理的行。

证书目录结构

签发后,真实文件落在 /etc/letsencrypt/:

/etc/letsencrypt/
├── live/<domain>/          # 每域名一张证书的"当前"链接目录(注意是软链,别删!)
│   ├── cert.pem          -> ../../archive/<domain>/cert1.pem
│   ├── chain.pem         -> ../../archive/<domain>/chain1.pem
│   ├── fullchain.pem     -> ../../archive/<domain>/fullchain1.pem
│   └── privkey.pem       -> ../../archive/<domain>/privkey1.pem
├── archive/<domain>/      # 每轮续期"历史版本"都保留在这,方便回滚
├── renewal/<domain>.conf  # 每张证书的续期配置
└── options-ssl-nginx.conf # certbot 的 TLS 安全参数

易错点:live/ 下全是软链接,指向 archive/。配置里永远引用 live/ 目录,不要引 archive/ 里的数字版本。这样每次续期后 live/ 自动指向最新版本,无需改配置。手动删除或 mv 这些链接会导致证书失效。

自动续期:certbot.timer

证书 90 天到期,但你不用手动续。certbot 用 systemd timer 每天运行两次,并在证书 剩余不足 30 天(renewal 配置里的 renew_before_expiry = 30 days)时才真正去续,其余时间跳过,节约 ACME 资源。

# 看本机 timer 是否启用(本机显示 active,每日两次)
systemctl status certbot.timer
systemctl list-timers | grep certbot

# 模拟续期:不真正向 CA 请求,只是走一遍全流程,验证配置/权限没问题
certbot renew --dry-run

本机 certbot renew --dry-run 的输出会显示四张证书全部 success——这是最该定期跑的健康检查。它证明域名验证、续期参数、部署钩子都在,真正的自动续期才靠谱。

续期配置长什么样

每张证书一份独立的 .conf,本机 bangdream.xyz 的续期配置:

# renew_before_expiry = 30 days
version = 2.9.0
archive_dir = /etc/letsencrypt/archive/bangdream.xyz
cert = /etc/letsencrypt/live/bangdream.xyz/cert.pem
privkey = /etc/letsencrypt/live/bangdream.xyz/privkey.pem
chain = /etc/letsencrypt/live/bangdream.xyz/chain.pem
fullchain = /etc/letsencrypt/live/bangdream.xyz/fullchain.pem

[renewalparams]
account = 18ea54f7687fd356cd91a72e4c5632c6
authenticator = nginx
installer = nginx
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa

key_type = ecdsa 表示本机用的是 ECDSA 证书——比 RSA 更短的密钥、更快的握手,TLS 1.3 下的现代选择。authenticator = nginx 说明续期时 certbot 会用它去写 nginx 验证端口。

场景案例:让 GitLab 自己的证书"闭嘴",统一给外层签发

服务器上的 GitLab(gitlab.bangdream.xyz)走的是外层 nginx 终止 TLS 的方案。GitLab 自己不开 Let's Encrypt,证书统一由外层 nginx 管:

浏览器 → 443/TLS → 外层 nginx(持有 cerbot 签的证书) → 内网 http://127.0.0.1:8090 → GitLab

对应 gitlab.rb 里的配置(本机实测):

# /etc/gitlab/gitlab.rb
external_url 'https://gitlab.bangdream.xyz'
nginx['listen_port'] = 8090          # GitLab 自己的 nginx 只监听内网端口
letsencrypt['enable'] = false        # 让 GitLab 不要自己签证书,交给外层

易错点:letsencrypt['enable'] = false 必须显式写,否则 GitLab 内置 certbot 会尝试自己签,和你的外层证书打架、还可能申请失败。同时记得在 gitlab.rb 改动后执行 gitlab-ctl reconfigure。这类"内层不自签"的委托模式,前提是外层 nginx 代理时把 X-Forwarded-Proto $scheme 正确传给内层,内层才知道该用什么协议。

安全提示:给内层服务(如 GitLab、后端 API)做 TLS 委托时,服务内部就走 http 127.0.0.1 明文是常见的——因为只在回环网卡上,不暴露公网,可以接受;但千万不要让内层服务监听 0.0.0.0,否则会绕过外层直接裸奔。

certbot 其他常用子命令

命令/工具 作用 示例
certbot --nginx -d <域> 用 nginx 认证器签发 certbot --nginx -d bangdream.xyz
certbot certonly 只签证书不改 nginx(与手动验证配合) certbot certonly --webroot -w /var/www/html -d x.com
certbot renew 对所有到期的证书执行续期 certbot renew
certbot renew --dry-run 模拟续期,验证流程 certbot renew --dry-run
certbot certificates 列出所有证书与到期时间 certbot certificates
certbot delete --cert-name <域> 删除某张证书 certbot delete --cert-name bangdream.xyz
certbot list 列出证书 certbot list

排查:续期失败时先看这几处

# 1. 证书到期时间
certbot certificates | grep -A3 bangdream.xyz

# 2. 模拟续期,看具体哪一步报错
certbot renew --dry-run

# 3. 是否被 rate limit(每域名每周签发有限次)
cat /var/log/letsencrypt/letsencrypt.log | grep -i "rate" | tail -5

# 4. 域名解析和 80 端口是否可达(Certbot 验证依赖)
dig +short bangdream.xyz; ss -ltnp | grep :80

易错点:HTTP-01 验证要用 80 端口,如果 80 被防火墙拦了或 nginx 没监听 80,ACME 验证必然失败。本机方案里 certbot 加的这段 80 -> 443 301 server 块,同时也保证 ACME 挑战能被正确响应。


下一章:03-nginx-ssl → 把 certbot 装好的 TLS 配置逐行拆开,搞懂 Nginx 到底在哪一层做了加密。