证书链、SNI、免费证书申请与自动续期,一次配好 HTTPS。
| 章节 | |
|---|---|
| 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 用非对称的手段协商出一把临时对称密钥,之后全程用对称加密传数据。
客户端 ──▶ ClientHello:我支持这些 TLS 版本和套件,SNI=www.example.com
◀── ServerHello:就用 TLS1.3 + AES_128_GCM_SHA256
◀── Certificate:这是我的证书(含公钥)
◀── CertificateVerify:用私钥签个名,证明证书是我的
客户端 验证证书链、有效期、域名是否匹配
──▶ 双方用 ECDHE 各自算出同一把会话密钥
──▶ Finished,之后全部用对称加密两个常被混淆的点:
选 RSA 还是 ECDSA:ECDSA 的 256 位密钥安全强度约等于 RSA 3072 位,握手更快、证书更小,现代客户端全都支持。兼容极老设备才需要同时配两套证书。
想算一下某段数据的摘要、或者理解签名里用到的哈希,用 MD5 / 哈希加密算法;带密钥的摘要用 HMAC 消息认证码生成。
浏览器不认识你的证书,它只认预装在系统里的几十个根证书。中间靠一条链连起来:
根证书(Root CA,系统内置,离线保管)
└─ 中间证书(Intermediate CA,实际签发用的)
└─ 你的证书(叶子证书 / 服务器证书)验证过程是自下而上:拿叶子证书里的签发者信息找到中间证书,用中间证书的公钥验签;再用根证书验中间证书的签名;根在本地信任库里,验证结束。
服务器必须把「叶子证书 + 中间证书」一起发给客户端,根证书不用发。 漏发中间证书是最常见的配置事故,表现非常迷惑:浏览器打得开(因为它会自己补全或缓存过),但 curl、Java 客户端、微信小程序、支付回调全报证书错误。
# 看服务端到底发了几张证书,-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 被封时用 |
# 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 是第二常见的事故,症状同上一章说的「浏览器能开但客户端报错」。
# 演练,确认续期流程能跑通(别用真实续期反复试,有频率限制)
certbot renew --dry-run
# certbot 装好后一般已经注册了 systemd timer,确认一下
systemctl list-timers | grep certbot证书换了要 reload 服务,续期钩子里加上 --deploy-hook "nginx -s reload"。
如果不想在服务器上装 ACME 客户端,免费 SSL 证书在线申请可以在页面上走完申请流程;需要自己生成 CSR 交给 CA 的,用 CSR 在线生成;内网、测试环境用自签名证书生成。
一份可以直接抄的现代配置:
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;。Strict-Transport-Security 一旦下发,浏览器在 max-age 内会强制用 HTTPS 访问该域,用户自己清不掉。上线前先用小的 max-age(比如 300)跑几天,确认所有子域都能 HTTPS 之后再加大、再考虑 includeSubDomains。preload 更是一旦进了浏览器预置名单,退出要等好几个版本。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 吊销查询。
一台服务器一个 IP 上跑多个 HTTPS 站点,靠的是 SNI(Server Name Indication):客户端在 TLS 握手的第一个包 ClientHello 里,用明文写上要访问的域名,服务器据此选择对应的证书。
这带来两个直接后果:
# 正确:显式指定 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 的那个)。建议显式放一个默认块,直接断开,避免拿别的站的证书去回应扫描器:
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 调用方全部报错、微信/支付宝回调直接失败。
# 打印到期时间
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 就告警:
# 每天 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查询网 原创,正文中的命令与配置片段可直接取用。 内容会随实践持续修订,发现错漏欢迎反馈。