TLS/HTTPS 部署实战:从 Let’s Encrypt 证书申请到自动续期的生产级链路 原创
引言
HTTPS 早已不是”加分项”,而是现代 Web 的默认配置——浏览器对纯 HTTP 站点标记”不安全”,搜索引擎降低排名,HTTP/2 等新协议更是只在 TLS 之上提供。但证书申请、链配置、续期、协议调优这条链路涉及不少细节,配置不当要么访问告警,要么埋下到期宕机的雷。本文以 Let’s Encrypt + certbot + Nginx 为主线,讲清从零部署一个生产级 HTTPS 站点的完整流程和排障方法。
一、TLS 握手与证书链:先搞清原理
浏览器访问 HTTPS 站点时,TLS 握手的关键步骤:
- 客户端发送支持的 TLS 版本、加密套件列表
- 服务端返回选定的套件和证书
- 客户端验证证书:域名是否匹配、是否过期、签发方是否受信任
- 客户端用证书里的公钥协商出会话密钥,之后通信全部对称加密
证书是一条信任链:
- 根 CA 证书:内置在操作系统/浏览器的信任库中(如 ISRG Root X1)
- 中间 CA 证书:由根 CA 签发,实际用来给站点签发证书
- 站点证书(叶子证书):CA 签发给你的域名的证书
关键点:服务端必须下发完整证书链(叶子 + 中间证书),只发叶子证书会让部分客户端因”无法补全到受信根”而报 unable to get local issuer certificate。Nginx 的 ssl_certificate 应指向 fullchain 而不是单独的 cert。
二、申请 Let’s Encrypt 证书
Let’s Encrypt 是免费、自动化的 CA,单证书有效期 90 天,靠自动化续期保证不过期。证明域名所有权有两种主流校验方式:
| 方式 | 原理 | 适用 |
|---|---|---|
| HTTP-01 | 在站点 /.well-known/acme-challenge/ 放校验文件,CA 通过 80 端口访问 |
有公网 Web 服务、80 端口可达 |
| DNS-01 | 在域名 DNS 添加一条指定 TXT 记录 | 通配符证书、80 端口不便暴露、内网场景 |
用 certbot 申请(Nginx 插件自动改写配置)
# 安装
apt install certbot python3-certbot-nginx
# 自动检测 Nginx 配置中的 server_name,申请并改写为 HTTPS
certbot --nginx -d example.com -d www.example.com
certbot 会完成:HTTP-01 校验、申请证书、修改 Nginx 配置增加 443 server 块、(可选)配置 80→443 跳转。
只申请证书,不改配置(webroot 方式)
certbot certonly --webroot -w /var/www/html -d example.com \
--email admin@example.com --agree-tos --no-eff-email
需要在 Nginx 预先保证 /.well-known/acme-challenge/ 可访问:
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
通配符证书(必须 DNS-01)
certbot certonly --manual --preferred-challenges=dns \
-d *.example.com -d example.com
# 按提示到 DNS 添加 _acme-challenge.example.com 的 TXT 记录
生产环境用 DNS 提供商的 API 插件(如 certbot-dns-cloudflare)实现全自动,避免手工加记录。
三、证书文件与 Nginx 配置
申请成功后,/etc/letsencrypt/live/example.com/ 下:
| 文件 | 内容 |
|---|---|
| cert.pem | 叶子证书(一般不单独用) |
| chain.pem | 中间证书链 |
| fullchain.pem | 叶子 + 中间,Nginx ssl_certificate 用这个 |
| privkey.pem | 私钥,Nginx ssl_certificate_key 用这个,严格保密 |
生产级 Nginx HTTPS 配置:
server {
listen 80;
server_name example.com www.example.com;
# 全部跳转到 HTTPS(ACME 校验路径除外,certbot 会自动处理)
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
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;
# 协议:只留 TLS 1.2 / 1.3,淘汰老旧版本
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# 会话复用,减少握手开销
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling:服务端主动取证书吊销状态,加速验证
ssl_stapling on;
ssl_stapling_verify on;
resolver 223.5.5.5 119.29.29.29 valid=300s;
resolver_timeout 5s;
# HSTS:告诉浏览器后续只走 HTTPS(先确认全站 HTTPS 再开)
add_header Strict-Transport-Security "max-age=31536000" always;
root /var/www/html;
index index.html;
}
改完检查并重载:
nginx -t && systemctl reload nginx
四、安全响应头
HTTPS 之外,几个响应头进一步加固:
| 响应头 | 作用 |
|---|---|
| Strict-Transport-Security | 强制浏览器后续使用 HTTPS,防降级/SSL 剥离 |
| X-Content-Type-Options: nosniff | 阻止 MIME 嗅探 |
| X-Frame-Options | 防止页面被 iframe 嵌套(点击劫持) |
| Content-Security-Policy | 限制脚本/资源来源,防 XSS(需按站点调,配错会挡正常资源) |
| Referrer-Policy | 控制 Referer 泄露 |
HSTS 上线要谨慎:一旦设置且 max-age 很长,期间若站点任何子路径无法走 HTTPS,用户将无法访问。建议先用小 max-age 验证,再逐步加大;确认所有子域名都 HTTPS 后才可考虑 includeSubDomains。
五、自动续期(最重要的运维环节)
Let’s Encrypt 证书 90 天到期,绝不能靠人工记得续。certbot 安装时通常已注册 systemd timer:
# 查看续期定时器
systemctl list-timers | grep certbot
systemctl status certbot.timer
# 模拟续期(不实际更换,验证流程能跑通)
certbot renew --dry-run
# 实际续期(一般由 timer 自动执行,每天检查,到期前30天内才真正续)
certbot renew
# 续期后重载 Nginx 使新证书生效
# certbot 默认通过 deploy hook 自动 reload,可确认:
ls /etc/letsencrypt/renewal-hooks/deploy/
推荐显式配置 deploy hook,保证续到新证书后 Nginx 一定重载:
echo '#!/bin/bash
systemctl reload nginx' > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
监控建议:把证书到期天数纳入监控(Prometheus probe_ssl_earliest_cert_expiry 或简单脚本 + 告警),在到期前 20 天仍未续就告警。自动续期 + 到期监控双保险,才能真正杜绝”证书半夜过期全站告警”。
六、验证与排障
# 查看线上证书链、有效期、签发者
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -issuer
# 查看服务端下发的证书链层数
echo | openssl s_client -connect example.com:443 -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
# 本地校验证书/链/私钥是否匹配(三个 modulus 应一致)
openssl x509 -noout -modulus -in fullchain.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
常见问题:
| 现象 | 根因 |
|---|---|
| 浏览器正常,curl/老客户端报证书错误 | 没下发中间证书,用了 cert.pem 而非 fullchain.pem |
| 续期失败(HTTP-01 timeout) | 80 端口被防火墙挡、webroot 路径不对、Nginx 未放行 acme-challenge |
| 证书申请报 too many certificates | 触发 Let’s Encrypt 签发频率限制,等限额重置或改用测试环境联调 |
| OCSP stapling 不生效 | 未配 resolver 或出站 53/UDP 被挡,stapling 是可选增强,不影响主流程 |
| 多域名证书只对一个域名有效 | 证书 CN/SAN 未覆盖全部域名,申请时所有域名都要通过 -d 列出 |
| 重载 Nginx 后仍是旧证书 | reload 未执行或浏览器/CDN 缓存,确认 deploy hook 和中间 CDN 的证书 |
七、有 CDN/反向代理时的注意点
如果站点在 CDN(Cloudflare 等)后面,存在两段 TLS:客户端↔CDN 由 CDN 证书负责,CDN↔源站由源站证书负责。这种”回源”链路:
- 源站证书可以是 Let’s Encrypt,也可以用 CDN 厂商的 Origin Certificate
- 不要在 CDN 开启”灵活(Flexible)”模式导致 CDN↔源站走明文 HTTP,应选”全严格(Full Strict)”,两段都加密并校验源站证书
- 站点是 Nginx 反向代理时,TLS 在最外层终结,后端 upstream 可以是 HTTP(内网可信),但跨机房/公网的回源仍应加密
总结
一条生产级 HTTPS 链路的关键决策:用 certbot 申请时选对校验方式(有 80 用 HTTP-01,通配符用 DNS-01 API 自动化);Nginx 必须用 fullchain 下发完整证书链;协议只留 TLS 1.2/1.3 并配 HSTS;自动续期 + deploy hook 重载 + 到期监控三件套保证永不宕机;CDN 场景选 Full Strict 保证回源也加密。最后用 openssl/curl 实测线上链和有效期,而不是只看配置文件——配置正确不等于线上生效。