技术学院 / SSL 证书与 HTTPS 部署

SSL 证书与 HTTPS 部署

进阶

证书链、SNI、免费证书申请与自动续期,一次配好 HTTPS。

6 章 · 16 节 · 约 10 分钟 · 更新于 2026-09-28 · 免费

章节
01 对称与非对称加密 1 小节
02 证书链与信任体系 2 小节
03 免费证书申请 3 小节
04 Nginx 配置 HTTPS 1 小节
05 SNI 与多域名 1 小节
06 证书到期监控 2 小节

对称与非对称加密

HTTPS = HTTP + TLS。TLS 要同时解决三件事:加密(别人看不到)、完整性(别人改不了且改了能发现)、身份(对面确实是那个域名)。这三件事分别由三类算法承担。

类别代表算法特点在 TLS 里干什么
对称加密AES-128-GCM、ChaCha20-Poly1305一把钥匙加解密,快加密实际传输的数据
非对称加密RSA、ECDSA公钥私钥一对,慢验证身份、签名
密钥交换ECDHE、DHE双方各算各的,算出同一个值协商出那把对称密钥
摘要SHA-256单向,定长输出签名与完整性校验

为什么要混用:对称加密快但没法安全地把钥匙递给对方;非对称能解决递钥匙的问题但太慢,用它加密整个会话性能扛不住。所以 TLS 用非对称的手段协商出一把临时对称密钥,之后全程用对称加密传数据。

握手大致发生了什么

text
客户端 ──▶ ClientHello:我支持这些 TLS 版本和套件,SNI=www.example.com
       ◀── ServerHello:就用 TLS1.3 + AES_128_GCM_SHA256
       ◀── Certificate:这是我的证书(含公钥)
       ◀── CertificateVerify:用私钥签个名,证明证书是我的
客户端      验证证书链、有效期、域名是否匹配
       ──▶ 双方用 ECDHE 各自算出同一把会话密钥
       ──▶ Finished,之后全部用对称加密

两个常被混淆的点:

  • 证书里只有公钥,没有私钥。证书是公开的,谁都能下载;私钥只在你服务器上,泄漏了就必须立刻吊销重签。
  • TLS 1.3 强制前向保密(PFS):会话密钥由 ECDHE 临时生成、用完即弃。即使日后私钥泄漏,之前抓到的流量也解不开。所以别再用 RSA 密钥交换的老套件。

选 RSA 还是 ECDSA:ECDSA 的 256 位密钥安全强度约等于 RSA 3072 位,握手更快、证书更小,现代客户端全都支持。兼容极老设备才需要同时配两套证书。

想算一下某段数据的摘要、或者理解签名里用到的哈希,用 MD5 / 哈希加密算法;带密钥的摘要用 HMAC 消息认证码生成。

证书链与信任体系

浏览器不认识你的证书,它只认预装在系统里的几十个根证书。中间靠一条链连起来:

text
根证书(Root CA,系统内置,离线保管)
  └─ 中间证书(Intermediate CA,实际签发用的)
       └─ 你的证书(叶子证书 / 服务器证书)

验证过程是自下而上:拿叶子证书里的签发者信息找到中间证书,用中间证书的公钥验签;再用根证书验中间证书的签名;根在本地信任库里,验证结束。

服务器必须把「叶子证书 + 中间证书」一起发给客户端,根证书不用发。 漏发中间证书是最常见的配置事故,表现非常迷惑:浏览器打得开(因为它会自己补全或缓存过),但 curl、Java 客户端、微信小程序、支付回调全报证书错误。

bash
# 看服务端到底发了几张证书,-showcerts 会打印整条链
openssl s_client -connect www.ip438.com:443 -servername www.ip438.com -showcerts </dev/null
 
# 只看有效期与主题
openssl s_client -connect www.ip438.com:443 -servername www.ip438.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

证书里有什么

字段说明
Subject / CN主体,早期放域名,现在只做展示
SAN(Subject Alternative Name)真正生效的域名列表,现代浏览器只看它
Issuer签发者,对应上一级证书
Not Before / Not After有效期
公钥算法与长度RSA 2048 / ECDSA P-256 等
签名算法SHA256withRSA 等,SHA-1 早已不可信

所以「证书域名不匹配」要去看 SAN 而不是 CN。

证书类型

类型验证内容签发速度适用
DV 域名型只验域名控制权分钟级,可免费绝大多数网站
OV 企业型额外验企业身份数天企业官网
EV 增强型最严格的企业核验更久金融类,但浏览器已不再显示绿色公司名

加密强度三者完全一样,差别只在身份核验的严格程度。另外还有通配符证书(*.example.com,只覆盖一级子域,不含根域本身,也不含 a.b.example.com)和多域名证书(SAN 里塞多个域名)。

站内工具:SSL 证书检测一次看全链、有效期、域名匹配和协议支持;证书信息查看把手里的 PEM/CRT 解出来看内容;链不完整时用证书链下载与补全补齐;概念性的问题可以先翻SSL 证书详解。

免费证书申请

现在免费 DV 证书是标配,Let's Encrypt、ZeroSSL 都走 ACME 协议自动签发,有效期 90 天,靠自动续期解决。

三种验证方式

方式原理适用
HTTP-01在 /.well-known/acme-challenge/ 下放一个文件最简单,但不支持通配符,且 80 端口要能被访问
DNS-01加一条 _acme-challenge 的 TXT 记录唯一能签通配符的方式,服务器不用暴露在公网
TLS-ALPN-01在 443 上用特殊 ALPN 协商只有 80 被封时用

用 certbot 签一张

bash
# nginx 插件:自动改配置并续期,最省事
certbot --nginx -d example.com -d www.example.com
 
# 只签证书不动配置(推荐,配置自己管)
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
 
# 通配符只能走 DNS-01
certbot certonly --manual --preferred-challenges dns -d '*.example.com' -d example.com

签出来的文件在 /etc/letsencrypt/live/example.com/:

文件内容nginx 用哪个
fullchain.pem叶子证书 + 中间证书ssl_certificate ← 用这个
cert.pem只有叶子证书别用,会导致链不完整
chain.pem只有中间证书OCSP stapling 用
privkey.pem私钥ssl_certificate_key

ssl_certificate 配成 cert.pem 是第二常见的事故,症状同上一章说的「浏览器能开但客户端报错」。

自动续期

bash
# 演练,确认续期流程能跑通(别用真实续期反复试,有频率限制)
certbot renew --dry-run
 
# certbot 装好后一般已经注册了 systemd timer,确认一下
systemctl list-timers | grep certbot

证书换了要 reload 服务,续期钩子里加上 --deploy-hook "nginx -s reload"。

如果不想在服务器上装 ACME 客户端,免费 SSL 证书在线申请可以在页面上走完申请流程;需要自己生成 CSR 交给 CA 的,用 CSR 在线生成;内网、测试环境用自签名证书生成。

Nginx 配置 HTTPS

一份可以直接抄的现代配置:

nginx
server {
    listen 443 ssl;
    http2 on;
    server_name www.example.com;
 
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
 
    # 只留 1.2/1.3,1.0/1.1 已被主流浏览器判为不安全
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;   # TLS1.3 下交给客户端选更好
 
    # 会话复用,减少完整握手
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
 
    # OCSP stapling:由服务器代取吊销状态,省掉客户端的一次外网查询
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 223.5.5.5 119.29.29.29 valid=300s;
 
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 
    root /var/www/html;
}

几个要点

  • http2 on; 是 nginx 1.25.1 起的写法,老版本写在 listen 行:listen 443 ssl http2;。
  • HSTS 要谨慎。Strict-Transport-Security 一旦下发,浏览器在 max-age 内会强制用 HTTPS 访问该域,用户自己清不掉。上线前先用小的 max-age(比如 300)跑几天,确认所有子域都能 HTTPS 之后再加大、再考虑 includeSubDomains。preload 更是一旦进了浏览器预置名单,退出要等好几个版本。
  • 反代场景下别忘了把协议信息传给后端,否则应用会以为自己在 http 下,生成出一堆 http 链接:
nginx
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;
  • 改完永远先 nginx -t 再 nginx -s reload,配置语法错了直接 reload 会让服务起不来。

不想手写的话,HTTPS 配置生成按服务器类型和兼容性档位生成配置;配完用 HTTP/2 与 TLS 协议检测确认协议和套件真的按预期协商;OCSP 是否正常用 OCSP 吊销查询。

SNI 与多域名

一台服务器一个 IP 上跑多个 HTTPS 站点,靠的是 SNI(Server Name Indication):客户端在 TLS 握手的第一个包 ClientHello 里,用明文写上要访问的域名,服务器据此选择对应的证书。

这带来两个直接后果:

  1. SNI 是明文的。加密保护的是 HTTP 内容,但「你访问了哪个域名」在握手阶段就暴露了(TLS 1.3 的 ECH 扩展想解决这个问题,尚未普及)。
  2. 测试时必须带对 SNI,否则服务器会返回默认站点的证书,看起来像配错了:
bash
# 正确:显式指定 SNI
openssl s_client -connect 1.2.3.4:443 -servername www.example.com </dev/null
 
# curl 的等价写法,Host 和 SNI 都会按域名来
curl -v --resolve www.example.com:443:1.2.3.4 https://www.example.com/

多域名的三种做法

做法说明注意
每域名一张证书 + SNI最常见每个 server 块各配各的
通配符证书 *.example.com覆盖所有一级子域不含根域,要签的话根域得单独写进 SAN;也不含二级子域
多域名(SAN)证书一张证书塞多个不相干域名一处变更要整张重签

nginx 里没匹配到 server_name 时会用第一个 server 块(或标了 default_server 的那个)。建议显式放一个默认块,直接断开,避免拿别的站的证书去回应扫描器:

nginx
server {
    listen 443 ssl default_server;
    ssl_reject_handshake on;      # nginx 1.19.4+
}

老客户端(Windows XP 的 IE、极老的 Java 6)不支持 SNI,遇到这种兼容要求只能一个 IP 一个站。现在基本不用考虑了。

顺带一提,邮件服务也有类似的 TLS 协商,用 邮件服务器 STARTTLS 检测 可以确认 MX 是否支持加密投递。

证书到期监控

免费证书 90 天有效期,意味着一年要换四次。没有监控的话,证书过期是站长第二常见的事故(第一是域名忘续)。过期的后果比想象的严重:浏览器整页拦截、API 调用方全部报错、微信/支付宝回调直接失败。

命令行检查

bash
# 打印到期时间
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate
 
# 判断是否还有 30 天以上(checkend 单位是秒)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 \
  && echo "还早" || echo "快过期了,赶紧续"

把它塞进 cron,剩余天数少于 20 就告警:

bash
# 每天 8 点检查一次
0 8 * * * /usr/local/bin/check-cert.sh example.com || /usr/local/bin/notify.sh

要盯的不止「到期」

检查项出问题的表现
剩余天数过期后整站拦截
证书链完整浏览器正常,API 客户端全挂
域名是否覆盖(SAN)新加的子域没签进去
协议与套件关了 TLS1.0 后老客户端连不上,或反过来仍开着不安全套件
OCSP / 吊销状态私钥泄漏后吊销是否真的生效
自动续期是否真的在跑certbot renew 因为 80 端口被占默默失败

最后一条特别隐蔽:续期任务失败往往没人看日志,等到过期才发现。所以监控要从外部探测线上实际返回的证书,而不是检查本地文件——文件更新了但服务没 reload,外面看到的还是旧证书。

日常用SSL 证书检测把域名过一遍,需要一次查一批就用批量网站巡检。证书格式在不同服务器之间倒腾(PEM / DER / PFX / JKS)用证书格式转换,私钥带密码需要加解密的用私钥密码加解密。

把域名到期、证书到期、服务器到期这三个日期放进同一个日历提醒,比任何监控系统都管用。

关于本文

本文由 ip438查询网 原创,正文中的命令与配置片段可直接取用。 内容会随实践持续修订,发现错漏欢迎反馈。