TLS/HTTPS 部署实战:从 Let’s Encrypt 证书申请到自动续期的生产级链路 原创

温馨提示:
本文最后更新于 2026-09-28,已超过 0 天没有更新。 若文章内的图片失效(无法正常加载),请留言反馈或直接 联系我。

引言

HTTPS 早已不是”加分项”,而是现代 Web 的默认配置——浏览器对纯 HTTP 站点标记”不安全”,搜索引擎降低排名,HTTP/2 等新协议更是只在 TLS 之上提供。但证书申请、链配置、续期、协议调优这条链路涉及不少细节,配置不当要么访问告警,要么埋下到期宕机的雷。本文以 Let’s Encrypt + certbot + Nginx 为主线,讲清从零部署一个生产级 HTTPS 站点的完整流程和排障方法。

一、TLS 握手与证书链:先搞清原理

浏览器访问 HTTPS 站点时,TLS 握手的关键步骤:

  1. 客户端发送支持的 TLS 版本、加密套件列表
  2. 服务端返回选定的套件和证书
  3. 客户端验证证书:域名是否匹配、是否过期、签发方是否受信任
  4. 客户端用证书里的公钥协商出会话密钥,之后通信全部对称加密

证书是一条信任链:

  • 根 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 实测线上链和有效期,而不是只看配置文件——配置正确不等于线上生效。