配了十年 SSL,还是能被一张过期的证书气到拍桌子。尤其是凌晨三点报警——网站突然全站 502,查半天才发现 kube-cert-manager 在自动续期的时候跟私有 CA 闹了别扭,没签发下来。说实话,那一刻真想回到 HTTP 明文裸奔的年代。不过话说回来,SSL(或者说 TLS,一回事儿)的复杂,正是它的工程美学所在——在不可信的公网上,靠一堆算法建立起信任,想想也挺燃的。

好多文章讲 SSL,贴个证书链的图就完事,看着困。我们倒过来,从握手这个重头戏切入。客户端发起 Client Hello——甩过去自己支持的最高 TLS 版本、一堆密码套件的编号、还有一个随机数。服务器回 Server Hello,敲定版本和密码套件,再扔回来自己的随机数和证书。光这一步就有个常见的坑——如果服务器端的密码套件配置过时,比如还开放着 TLS_RSA_WITH_AES_128_CBC_SHA,那么恭喜你,前向安全性直接归零。因为 RSA 密钥交换一旦私钥未来泄露,所有历史流量都能被解密。所以现在正经部署都要求 ECDHE 作为密钥交换,那个“E”代表临时,每次握手都动态生成密钥对,哪怕主私钥丢了,历史会话也安全。
接着是证书验证。客户端拿到证书后,会沿着信任链一直查到内置的根证书。这中间有个数字签名的校验——CA 用自己的私钥对证书的哈希做了签名,客户端拿 CA 公钥解开,比对一下哈希。如果链上的某张中间证书没预置在客户端,那就爆出那个经典的“此网站的安全证书不受信任”。踩过的都知道,有时候是服务器只配了叶子证书,没把中间证书拼接上,要用 cat your_domain.crt intermediate.crt > combined.crt 的方式补全链,或者用 acme.sh 自动处理。

非对称的那点事:RSA 的黄昏与 ECC 的逆袭
聊到非对称加密,RSA 是祖师爷级别。原理简单说就两要素:大数分解难题。选俩大质数 p 和 q,乘出 n,再算个欧拉函数 φ(n),挑个跟 φ(n) 互质的 e,求出 d 使得 ed ≡ 1 (mod φ(n))。加密就是求密文 c = m^e mod n,解密 m = c^d mod n。听着玄乎,但核心就在模幂运算上。2048 位的 RSA,n 长达 617 个十进制位,分解它需要指数级的时间。不过随着硬件发展,现在看来也不那么安全了,Google 已经在 Chrome 里逐步提高对 RSA 密钥的长度要求。
ECC 就走了一个完全不同的路:基于椭圆曲线上的离散对数问题。选定一条曲线,比如 secp256r1,私钥是 256 位随机数 d,公钥是点 Q = d * G(G 是基点,* 是曲线上的标量乘法,本质是倍点运算)。攻击者要从 Q 反推 d,得走通 Pollard’s rho 之类方法,复杂度 O(√n),对于 256 位密钥就是 2^128 次操作,对比 RSA 2048 的约 2^112 次操作,安全性略高,密钥却短得多。实际压测数据更说明问题:在单核 Xeon E5-2680 上,用 OpenSSL 1.1.1,RSA 2048 的签名速度约为 1200 次/秒,验证约 14000 次/秒;而 ECDSA P-256 签名能飙到 17000 次/秒,验证近 6000 次/秒——对于服务器端主要做签名的场景(比如 TLS 握手时对 ephemeral 公钥签名),ECC 的性能压倒性优势。这就是为什么 Let’s Encrypt 早在 2016 年就默认签发 ECC 证书,而大型 CDN 的边缘节点全切到了 ECDSA 证书。
会话恢复的聪明与糊涂

SSL 握手一次的成本不小,非对称运算吃 CPU。所以有了“会话恢复”——这玩意儿我总想起“咱们上次聊到哪儿了”的梗。经典做法是 Session ID:服务器在首次握手后缓存会话参数,并给客户端一个 ID,下次客户端发 Client Hello 时带上这个 ID,双方就能跳过证书和密钥交换,直接恢复加密。坑来了:如果服务器集群没有共享 Session 缓存(比如没用 Redis 或 memcached 存 session),那请求落到别的节点就认不出 ID,只能重新完全握手,性能瞬间雪崩。我曾经在压测一个电商网站时,观察到 Nginx 默认的共享内存 session cache 在跨 worker 时就有这种不一致,后来全部换成使用session tickets——加密的会话参数由客户端保存,服务端解密后恢复,无状态分发,QPS 提升了 30% 以上。但是,ticket 密钥如果长期不轮换,又会损伤前向安全性,所以得定期更换(比如每 12 小时),配合 nginx 的 ssl_session_ticket_key 指令。
落地三连坑:过期、混合内容、协议降级

坑一:证书过期自动化救不了所有情况。Let’s Encrypt 普及后,很多人以为配个 Certbot 就高枕无忧。但如果你有内部域名、或者由于策略不能用 ACME DNS 验证的通配符证书,就只能手工维护。解决方案:搭建内部 ACME 服务(如 Smallstep),并配合 Prometheus 监控证书到期(告警阈值设 15 天),用 cert-manager 部署在 Kubernetes 里,若处于 air-gapped 环境则使用 Vault 的 PKI 引擎定期短有效期签发。
坑二:混合内容——页面中加载了 HTTP 资源,浏览器锁图标消失或直接拦截。尤其是广告图片、老旧 CDN 的字体文件,往往埋得很深。除了全站 CSPh 头里加 upgrade-insecure-requests,我还习惯在 Nginx 用 subs_filter 替换内嵌的 http:// 链接,但更彻底的是开启 Content-Security-Policy-Report-Only,收集违规报告,然后一步步修复。曾经花了一周清洗一个门户站,最后发现是第三方客服聊天插件硬编码了 http 的 WebSocket……这种就只能替换供应商。
坑三:协议降级攻击,也就是让客户端回退到低版本 TLS 或弱密码套件。服务器端要坚决禁掉 TLS 1.0/1.1,并在 Nginx 的 ssl_ciphers 里只留 ECDHE 开头的套件,配上 ssl_prefer_server_ciphers on;。用 SSL Labs 扫描能有 A+,但真正要防的是中间人诱导降级——HSTS 头设置 max-age=31536000; includeSubDomains; preload,并且申请加入浏览器预加载列表,可一劳永逸。
SSL 这摊事,往深了走还有 HPKP 被废弃后的 Expect-CT、证书透明度日志、后量子密码迁移…… 每次觉得搞明白了,新漏洞就出来打脸。不过,这也是它迷人的地方——在混乱中建立秩序,靠数学和工程的双重严谨,撑起互联网的信任基石。行了,该去检查下个月的证书到期告警了。