← 返回博客首页

HTTPS 证书完全指南:从 TLS 握手到自动化续期

HTTPS 到底保护了什么

很多人以为 HTTPS 只是"加密",实际上它一次性提供三件事,三者缺一不可:

能力 解决的问题 依赖的技术
机密性 中间人不能直接读到传输内容 对称加密(AES-GCM / ChaCha20-Poly1305)
完整性 数据被篡改能被立刻发现 AEAD 认证标签 / HMAC
身份认证 你连上的确实是 example.com 而不是假站 证书链 + CA 签名

第三点是新手最容易忽略的。没有身份认证的加密毫无意义——你和攻击者之间照样可以建立一条"加密"的通道,只是对端是攻击者而已。证书的本质,就是一个可信第三方(CA)为你"这个域名属于你"这件事背书。

想验证自己有没有把响应头配错,可以对照本站的 HTTP 头速查表 逐条检查;端口层面的常识(443、80 跳转)见 常见端口速查表

TLS 1.3 握手时序

TLS 1.2 需要 2-RTT 才能开始发数据,TLS 1.3 把它压到 1-RTT,甚至 0-RTT(代价是重放风险)。完整流程如下:

客户端                                            服务端
   |                                                  |
   |--- (1) ClientHello ------------------------------>|
   |      支持的 TLS 版本、密码套件列表                |
   |      key_share: 客户端的 (EC)DHE 公钥             |
   |      支持的签名算法、ALPN(h2/http1.1)             |
   |      SNI: 目标主机名                              |
   |                                                  |
   |<-- (2) ServerHello -------------------------------|
   |      选定的版本与密码套件                         |
   |      key_share: 服务端的 (EC)DHE 公钥             |
   |      {证书链}  ← 服务端身份认证                    |
   |      {CertificateVerify} ← 用证书私钥签名握手摘要  |
   |      {Finished}                                   |
   |                                                  |
   |--- (3) Finished + 应用数据 ---------------------->|
   |                                                  |
   |<== 应用数据(对称加密) =========================>|

几个关键点值得单独拎出来:

  • key_share 在 ClientHello 阶段就发出去了。这是 TLS 1.3 比 1.2 快一个 RTT 的根本原因:密钥交换材料第一趟就带上,不等服务端 round trip。
  • CertificateVerify 才是真正的身份证明。证书本身是公开的,谁都能转发;服务端必须用证书对应的私钥,对截至此刻的握手记录做一次签名,客户端用证书里的公钥验签通过,才能证明"对端确实持有私钥"。
  • 证书是明文传输的。它本来就是公开信息,不需要保密,只需要防篡改(由签名保证)。所以不要在证书里放任何敏感信息。
  • SNI 也是明文。观察者能看到你访问了哪个域名(这是 ECH / Encrypted Client Hello 要解决的问题,目前普及率仍有限)。

证书链:信任是怎么传递的

浏览器本地只预置了约一百多个根 CA 证书。你的证书不直接由根 CA 签,而是由中间 CA 签,中间 CA 再由根 CA 签。这么做是为了安全——根 CA 的私钥可以离线冷存储在硬件安全模块里,日常签发全部交给中间 CA,一旦中间 CA 泄露只需吊销它,不至于动摇整个根。

根 CA(离线存储,预置在操作系统/浏览器)
 └── 中间 CA(在线签发,如 R3 / E1)
      └── 叶子证书(你的域名,如 oltool.net)

验证时浏览器从叶子往上走,逐层验签,直到能连上本地某个根证书为止。这带来两个实操要点:

  1. 必须部署 fullchain,不能只发叶子证书。服务端要把中间证书一起发过去。漏了中间证书就是最常见的不信任故障——详见文末排查表。
  2. 交叉签名会造成"多路径"。同一个中间证书可能同时被两个根签过(新旧根过渡期),浏览器会尝试所有路径,只要有一条走得通就算可信。这解释了为什么某些老设备能访问、新设备反而报错。

证书类型怎么选

按验证等级

等级 验证内容 签发时间 地址栏表现 适用场景
DV 域名验证 你能控制这个域名 秒级~分钟 小锁图标 博客、工具站、API、绝大多数站点
OV 组织验证 域名 + 真实存在的组织(人工审核营业执照) 1–3 天 小锁 + 证书里可查组织名 企业官网、B2B 场景
EV 扩展验证 最严格的法律实体核查 1–2 周 现代浏览器已不再显示绿色公司名 金融、支付(合规要求时)

EV 的实际价值已经大幅缩水:Chrome 与 Firefox 先后移除了地址栏的绿色组织名显示,用户看不出区别。除非你的合规审计明确要求 EV,否则 DV + 好的安全实践更划算。

按覆盖域名

类型 覆盖范围 示例 注意
单域名 一个精确主机名 www.oltool.net 通常自动附带裸域或 www,签发前确认
通配符 同一级下所有子域 *.oltool.net 不匹配多级a.b.oltool.net 不在覆盖内
多域名 SAN 任意多个主机名(可跨域名) a.com + b.net + *.c.org 清单写进 SAN 扩展,扩容需重签

通配符证书有个实践上的隐患:它让私钥的分发面变大。一张 *.example.com 的私钥要放到所有子域的机器上,任何一台失守都会波及全部子域,且吊销后影响面巨大。大型部署更推荐按服务签发独立证书 + 自动化管理,而不是图省事用一张通配。

获取证书:三条路径

路径 成本 有效期 自动化 适用
Let's Encrypt / ACME 免费 90 天 完善(HTTP-01 / DNS-01 / TLS-ALPN-01) 公网 Web 服务,首选
商业 CA 付费 最长约 1 年(持续缩短中) 视厂商而定 需要 OV/EV、保险、IP 证书、合规审计
自签 / 私有 CA 免费 自定 自己搭 内网、服务间 mTLS、开发测试

ACME 的验证方式怎么选

  • HTTP-01:在 http://<域名>/.well-known/acme-challenge/ 放一个文件让 CA 来取。简单,但要求 80 端口可达,且不能签发通配符证书
  • DNS-01:在 DNS 里加一条 _acme-challenge 的 TXT 记录。稍麻烦(需要 DNS 服务商 API 凭据),但支持通配符证书,且不要求服务器公网可达——内网机器也能签。生产环境推荐走这条。

私钥的生成可以完全在本地浏览器完成(不上传、不经过网络),比如用本站的 RSA 密钥生成;需要校验摘要或比对指纹时用 哈希生成,算法本身的强度对照见 哈希与加密算法速查表

私钥与算法:RSA 还是 ECDSA

维度 RSA 2048 RSA 3072 ECDSA P-256 Ed25519
等效安全强度 112 bit 128 bit 128 bit 128 bit
公钥长度 256 B 384 B 64 B 32 B
握手开销
老客户端兼容 极好 Android 4+/多数现代环境 较差
推荐度 仅作兼容回退 一般 首选 实验性

结论:优先 ECDSA P-256,兼容性包袱重时配一张 RSA 做双证书。Nginx 里就是两行,现代客户端自动协商到 ECDSA:

ssl_certificate     /etc/letsencrypt/live/oltool.net/fullchain.pem;   # ECDSA
ssl_certificate_key /etc/letsencrypt/live/oltool.net/privkey.pem;
ssl_certificate     /etc/letsencrypt/live/oltool.net-rsa/fullchain.pem; # RSA 回退
ssl_certificate_key /etc/letsencrypt/live/oltool.net-rsa/privkey.pem;

Nginx 部署:一份能直接用的配置

更多上下文(反向代理、静态资源、location 匹配规则)见 Nginx 配置速查表

server {
    listen 80;
    listen [::]:80;
    server_name oltool.net www.oltool.net;
    # ACME 挑战保持明文可访问,其余全部跳转 HTTPS
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name oltool.net www.oltool.net;

    # fullchain 包含中间证书,切勿只填叶子证书
    ssl_certificate     /etc/letsencrypt/live/oltool.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/oltool.net/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;            # TLS 1.0/1.1 已全部废弃
    ssl_prefer_server_ciphers off;            # 让客户端优先,服务端不强制
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;                  # 关闭 ticket 以避免密钥长期复用

    # OCSP stapling:由服务端代客户端去问 CA,省一次外部请求
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # HSTS:强制浏览器后续只走 HTTPS。max-age 先从短值起步,确认无误再加长到 31536000
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

三个容易被忽略的坑:

  1. add_header 不会继承。在 location 块里一旦写了任何 add_header,上层的 add_header(包括 HSTS)就会被整体丢弃,必须在需要的每一层重写一遍。加上 always 确保 4xx/5xx 响应也带上。
  2. HSTS 不可逆。一旦发出 max-age=31536000,在整整一年内你无法让用户回落到 HTTP。上生产前先确认全站资源都能走 HTTPS;首次部署建议先用 max-age=300 试水。
  3. includeSubDomains 是陷阱。它对所有子域生效,如果某个内网子域没有 HTTPS,加了这条之后你会直接连不上它。确认全部子域都就绪后再加。

故障排查表

症状 根因 排查命令 修复
浏览器不信任,桌面正常、手机报错 证书链不完整 openssl s_client -connect host:443 -servername host 看 Certificate chain 段深度 改用 fullchain.pem
NET::ERR_CERT_COMMON_NAME_INVALID 证书 SAN 不含当前主机名 openssl x509 -in cert.pem -noout -text | grep -A1 "Subject Alternative Name" 重签,补全 SAN
ERR_CERT_DATE_INVALID 过期或本机时间错误 openssl x509 -in cert.pem -noout -dates 续期 / 校准时钟
页面小锁带感叹号 混合内容:HTTPS 页里加载了 http:// 资源 浏览器控制台 Mixed Content 提示 / DevTools Network 过滤 改协议相对或全站 https
只有部分客户端握手失败 算法或 TLS 版本不兼容 openssl s_client -tls1_2 / curl --tlsv1.2 放宽协议或加 RSA 双证书
多站点同 IP,返回了错证书 SNI 未生效 / 缺少 default_server openssl s_client -connect ip:443 -servername a.com 对比两次结果 server_name 与默认 server
OCSP 响应超时拖慢首连 CA 的 OCSP 端点被墙或不可达 openssl s_client -status 看 OCSP Response Status 开启 stapling 或换 CA
证书被吊销但客户端仍放行 未配置 stapling 且客户端不做实时校验 openssl x509 -noout -text 查 CRL/OCSP 地址 开 stapling + 缩短有效期

生命周期与自动化

一份能长期不管的证书体系,靠的是三件事:

  1. 自动续期。ACME 客户端 + 定时器,每天检查,到期前 30 天自动续,续完 reload:
    # systemd timer(比 cron 更好:可查看上次运行结果与日志)
    systemctl enable --now certbot-renew.timer
    
  2. 到期监控。续期会失败(DNS API 变更、端口被挡、额度超限),所以必须独立监控剩余天数,低于 21 天就告警。没有监控的自动续期等于没有自动续期。
  3. 私钥管控。私钥权限 600、属主 root;不要进 git(进 git 就等于永久泄露,即使后续删除);备份要加密;怀疑泄露立即吊销重签。

上线检查清单

  • [ ] 使用 fullchain.pem(含中间证书),而非叶子证书
  • [ ] 仅启用 TLS 1.2 / 1.3,关闭 TLS 1.0 / 1.1
  • [ ] SAN 覆盖所有会被访问到的主机名(含裸域与 www)
  • [ ] 80 端口 301 跳转到 HTTPS,且放行 /.well-known/acme-challenge/
  • [ ] 无混合内容(页面内所有资源均为 https)
  • [ ] HSTS 已从短 max-age 起步验证
  • [ ] OCSP stapling 开启且可验证(openssl s_client -status
  • [ ] 自动续期已生效并有独立到期监控
  • [ ] 私钥权限 600,未提交进版本库
广告

常见问题

免费证书和付费证书在加密强度上有区别吗?

没有。两者使用完全相同的加密算法与密钥长度,浏览器对小锁图标的处理也一致,中间人无法从流量里看出你用的是免费还是付费证书。真正的区别在于:验证等级(免费 CA 通常只提供 DV,OV/EV 需付费)、有效期(免费证书普遍 90 天,付费可到 1 年)、保险赔付、以及 wildcard 与 IP 证书等品类的可得性。对绝大多数站点,免费 DV 证书加密强度已足够。

浏览器提示证书不受信任,但证书明明没过期,最可能是什么原因?

最常见的原因是**证书链不完整**:服务器只发送了叶子证书,漏了中间 CA 证书。浏览器本地只有根证书,无法凭空推导出中间证书,于是判定不可信。修复方式是部署时把中间证书与叶子证书拼进同一个文件(fullchain)。注意这个故障有迷惑性:多数桌面浏览器会用缓存或 AIA 扩展自动补齐中间证书从而正常显示,但移动端、curl、以及各类 API 客户端会直接失败,所以不能只靠浏览器验证。

RSA 和 ECDSA 证书应该选哪个?

优先选 **ECDSA(P-256)**,除非你有明确的兼容性包袱。P-256 的安全强度约等于 RSA 3072,但密钥更短、握手更快、CPU 与带宽开销更低,对移动端和高并发站点收益明显。代价是极老的客户端(Android 4.x 之前、Windows XP、部分老 Java 6/7)不支持 ECDSA。稳妥做法是双证书部署:同时配置 ECDSA 与 RSA 证书,现代客户端协商到 ECDSA,老客户端自动回落到 RSA,Nginx 1.11+ 原生支持。

证书多久续期一次?90 天有效期会不会太短?

90 天是行业趋势而非缺陷——CA/Browser Forum 已通过决议,最长有效期将逐步缩短到 47 天。短有效期能限制私钥泄露后的暴露窗口,并倒逼自动化。正确做法不是买长有效期证书,而是把续期彻底自动化:用 ACME 客户端(certbot / acme.sh)配合 systemd timer 或 cron,每天检查一次、到期前 30 天自动续期,续期后 reload 服务。再叠加到期监控(剩余天数低于 21 天告警),人就不用再管这件事。

← 返回博客首页