Let's Encrypt 签发与 Nginx HTTPS 全流程实战:从 ACME 到自动续期
上一篇文章把 TLS 1.3 的握手与证书链讲透了,但「怎么真正给一个线上域名配上可用的 HTTPS」还停留在纸面。本文不是教程复读,而是把本站 www.keenth.top 从「裸 HTTP」迁到「HTTPS + HSTS + 自动续期」的完整实操实录:ACME 校验方式怎么选、为什么踩到 nginx 的 add_header 继承坑、自动续期要配到哪一步才算「真自动」。每一步都给出可复制的命令,并解释「为什么这么做」。
一、选校验方式:HTTP-01 与 DNS-01 的取舍
签发前必须回答一个问题:用什么方式向 CA 证明域名归属。三种常见挑战方式有本质区别:
| 方式 | 怎么验证 | 端口要求 | 适用场景 |
|---|---|---|---|
| HTTP-01 | 在 /.well-known/acme-challenge/ 放一个 token,CA 用 HTTP 拉取 | 80 对外开放 | 绝大多数公网站点(推荐) |
| DNS-01 | 把 token 写进域名的 _acme-challenge TXT 记录 | 仅需 DNS 控制权 | 不能开 80、泛域名 *.example.com 通配符 |
| TLS-ALPN-01 | 在 443 用特殊 ALPN 协议回显 token | 443 可控 | 需要证书又不能动 80(少见) |
本站 80 端口对外开放、也不需要通配符,所以选 HTTP-01。它有个隐藏的红利:CA 会从「公网视角」请求你的域名,一旦 HTTP-01 能通过,基本证明「这个域名在全球都能被访问到」,顺带帮你把 DNS 可达性测了一遍。
一个很容易被忽略的坑:./.well-known 不能被拦截。如果你的 Nginx 写了 location ~ /\. { deny all; } 之类的「点文件一律拒绝」规则,HTTP-01 会直接 404 导致签发失败。正确写法要留出 .well-known,用负向断言或单独放行——后面加安全规则时务必留意这一点。
二、webroot 签发:一条命令拿到证书
装好 certbot 后,我用 webroot 方式签发:
apt-get install -y certbot
certbot certonly --webroot -w /var/www/techpulse \
-d www.keenth.top \
--non-interactive --agree-tos --email admin@keenth.top
# 输出:Certificate is saved at: /etc/letsencrypt/live/www.keenth.top/fullchain.pem
-w /var/www/techpulse 指定 webroot 目录,certbot 会在其下创建 .well-known/acme-challenge/ 写 token。因为我的 techpulse vhost 是 server_name _ 的默认站,任何 Host 落到这台机 80 端口都会被这个站处理,所以挑战文件能被拉取到。这里同样踩过一个错:第一次我把 keenth.top(不带 www)一起签,但 apex 域名根本没配 DNS A 记录,CA 连不上挑战文件,整个签发失败。教训是——要签的每一个域名,都必须先确认它能被公网解析并访问,否则一颗老鼠屎坏一锅粥。
三、443 配置与「TLS 安全基线」
证书拿到后,改 Nginx。核心是让同一份站点内容同时服务 80 和 443,并对 www 强制跳 HTTPS:
server { # 闪跳块
listen 80;
server_name www.keenth.top;
return 301 https://$host$request_uri;
}
server { # 主站,80 默认 + 443
listen 80 default_server;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/www.keenth.top/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.keenth.top/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
}
三个值得说的点:
为什么用闪跳块而不是主块加 return。主块是 default_server,负责所有「没匹配上特定 server_name」的 Host(包括 IP 直连)。如果直接在主块里 return 301,会误伤其他 Host。所以只对 www.keenth.top 这个精确 server_name 加闪跳,其余走 HTTP 原样服务,互不干扰。
TLS 1.2 + 1.3 够用,够稳。1.0/1.1 早就因 POODLE/BEAST 被浏览器弃用;只开 1.2 和 1.3,是安全与兼容性的甜点位。ssl_prefer_server_ciphers off 表示优先客户端算好密码套件(配合 AEAD + TLS1.3 默认 PFS),避免被降级。
会话复用别省。ssl_session_cache shared:SSL:10m 多 worker 进程共享会话缓存,能显著降低 TLS 握手开销;ssl_session_tickets off 关闭无状态的 SessionTicket(RSA 密钥重协商的旧机制),减小密钥泄露面。
四、那个差点让 HSTS 静默失效的坑:add_header 继承
按很多教程,安全头要么写在 server 层级,要么会推荐「再加一个 HSTS」。但我在 server 层加了 add_header Strict-Transport-Security ... always 后验证时,发现页面响应里根本没有 HSTS。排查过程本身就是一个经典案例:
# 现象:全页面的响应里,X-Frame-Options 在,HSTS 不在
# 直觉是 HSTS 拼错了?看完整响应头:
curl -sI https://www.keenth.top/
# 发现 Cache-Control 出现两个:no-cache 和 no-cache, must-revalidate
两条 Cache-Control 暴露了真相:请求 / 会被 index 指令内部重写为 /index.html,进而命中 location ~* \.html$。而 nginx 的 add_header 有个继承规则:只要某个 location 自己写了 add_header,server 层级的 add_header 就不会被继承。我的 .html 和 /assets/ 两个 location 各自写了自己的安全头,于是把 server 层级的 HSTS 全部「顶掉」了。
这就是 nginx 一个非常隐蔽、教科书上常被忽略的规则:add_header 采用「就近覆盖」,不属于严格继承。正确修法是让每个实际输出响应的 location 都补齐全套安全头(含 HSTS),而不是指望 server 层一处定义全局生效。这也是排HTTP安全头问题时的第一排查顺序:先确认响应到底命中了哪个 location,再看那个 location 有没有自己的 add_header。
五、自动续期:不只是装个 timer 那么简单
Let's Encrypt 证书只有 90 天,自动续期才算真正「装上 HTTPS」。certbot 装好时会自动注册 systemd timer(certbot.timer),每天跑 certbot renew,但有个关键细节容易被忽略——续期后 Nginx 不会自动重新加载。
证书文件路径是符号链接:/etc/letsencrypt/live/www.keenth.top/fullchain.pem → ../../archive/.../fullchainN.pem,续期会生成 newN 并更新这个软链。Nginx 进程在 reload 前读到的还是内存里旧证书。所以要加一个 deploy-hook,在每次成功续期后重载 Nginx:
mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh
renewal-hooks/deploy 下的脚本只会在续期成功后执行(失败的续期不会触发),语义上比把 reload 写进 cron 干净。最后用 certbot renew --dry-run 做一次演练,确保「到点真能续上」。验证命令:
openssl x509 -in /etc/letsencrypt/live/www.keenth.top/cert.pem \
-noout -dates -subject -ext subjectAltName
# 输出 notAfter 应落在 90 天后,SAN 包含 www.keenth.top