1. 从一次安全扫描告警说起
前阵子帮一个朋友处理他们内部测试环境的安全扫描报告,扫出来一条看着挺唬人的告警:服务器支持 TLS Client-initiated 重协商攻击(CVE-2011-1473)。朋友第一反应是“这编号都十几年前的了,是不是扫描器误报”,第二反应是“要不要直接关掉重协商了事”。我把他拦住了——这个漏洞虽然老,但处理方式如果一刀切,很可能把某些还在用老客户端的业务直接搞挂。
这篇文章就把我这次修复的完整过程摊开讲清楚:这个 CVE 到底是怎么回事、为什么十几年了还在被扫出来、不同服务(Nginx、Apache、Tomcat、OpenSSL 自研服务)分别怎么修、修完怎么验证、以及哪些坑我踩过。如果你手上也有一份类似的扫描报告,或者你负责的服务器被要求做等保、做安全加固,这篇可以直接拿去抄作业。
先说结论,方便赶时间的同学:这个问题的本质不是“重协商”本身有漏洞,而是“客户端发起的重协商”会带来服务端 CPU 被恶意消耗的风险。修复的核心思路是禁用客户端发起的重协商,或者限制重协商的频率,而不是把整个 TLS 重协商机制干掉。下面我按“原理—选型—实操—验证—排坑”的顺序展开。
2. 这个十几年前的 CVE 到底在说什么
2.1 重协商机制本身是干嘛的
要理解这个漏洞,得先搞清楚 TLS 重协商(Renegotiation)是干什么的。TLS 握手建立连接之后,双方会协商出一套会话密钥。但有些场景下,连接建立之后还需要重新协商一套新的密钥或者新的参数,比如:
- 客户端在握手时没有提供客户端证书,服务端需要客户端证书做二次认证,于是发起一次重协商,要求客户端补上证书;
- 长时间连接需要定期更换会话密钥,提升前向安全性;
- 某些老旧的协议交互需要在连接中途切换加密套件。
重协商分两种发起方:服务端发起(Server-initiated)和客户端发起(Client-initiated)。前者是服务端主动要求重新握手,后者是客户端在已经建立的连接上主动发起新的握手。
2.2 漏洞的真正成因
CVE-2011-1473 的核心问题在于:服务端允许客户端在一条已建立的连接上反复发起重协商,而每次重协商都需要服务端做大量的非对称加密运算(RSA 签名/验签、DH 密钥交换等),这些运算的 CPU 开销远大于普通的数据传输。
攻击者可以在一台普通机器上,用相对很小的带宽,向服务端发起大量重协商请求,让服务端的 CPU 被打满,形成一种“低带宽高消耗”的拒绝服务效果。这就是所谓的Client-initiated Renegotiation DoS。
用生活化的类比:正常的数据传输像是“两个人已经通了电话在聊天”,而重协商相当于“每聊一句就重新拨号一次”。攻击者就是不停地要求重新拨号,让你接线员(CPU)忙到崩溃,而他自己付出的成本极低。
2.3 为什么十几年了还在被扫出来
很多人会问,2011 年的漏洞,OpenSSL 早就打过补丁了,为什么现在扫描还能扫出来?原因有几个:
- 补丁修的是“无限重协商”,不是“重协商本身”。OpenSSL 1.0.1 之后默认限制了重协商的次数和频率,但很多服务配置里仍然显式允许客户端重协商,扫描器只要探测到“服务端接受客户端发起的重协商”就会报。
- 扫描器的判定标准比较保守。它不判断你是否已经限制了频率,只要你能接受一次客户端发起的重协商,就标记为“支持”,然后报 CVE-2011-1473。
- 很多中间件默认配置就是允许的。比如某些版本的 Apache、某些自研的基于 OpenSSL 的服务,默认没有关掉客户端重协商。
所以这条告警的准确含义是:你的服务端当前接受客户端发起的重协商,存在被 DoS 的潜在风险。至于风险有多大,取决于你有没有做频率限制、有没有前置的流量清洗、业务是否对延迟敏感。
3. 修复方案选型:为什么不能一刀切
3.1 三种可选思路
面对这个告警,通常有三条路:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 方案A:完全禁用客户端重协商 | 配置层面关掉客户端发起的重协商 | 彻底消除风险,配置简单 | 依赖客户端证书二次认证的业务会挂 |
| 方案B:限制重协商频率 | 保留重协商但限制次数/速率 | 兼容性好 | 需要中间件支持,配置复杂 |
| 方案C:前置卸载+流量清洗 | 在负载均衡层做限制 | 对后端透明 | 依赖架构,成本高 |
3.2 我的选型逻辑
我这次处理的环境是一个内部测试平台,业务方明确说“没有依赖客户端证书二次认证的场景”,所有客户端都是现代浏览器和较新的 SDK。这种情况下,方案A 是最优解:风险彻底消除,配置改动最小,验证也最简单。
但如果你的环境里有以下情况,就要慎重:
- 有老旧的 Java 客户端、老版本 .NET 客户端依赖重协商做认证;
- 有基于客户端证书的双向认证(mTLS)业务,且证书是在连接建立后才要求提供的;
- 有长连接业务依赖定期重协商换密钥。
这些场景下贸然禁用客户端重协商,会导致握手失败、连接中断。我的建议是先在测试环境验证,用真实客户端跑一遍,确认没有依赖后再上生产。
提示:判断业务是否依赖客户端重协商,最直接的办法是在测试环境禁用后观察一段时间,看是否有 TLS 握手失败、连接异常断开的日志。不要凭感觉判断。
3.3 关于“限制频率”方案的补充
如果你的环境确实不能完全禁用,方案B 是折中。OpenSSL 层面可以通过SSL_CTX_set_options设置SSL_OP_NO_RENEGOTIATION(完全禁用)或者通过回调限制重协商次数。但要注意,不同 OpenSSL 版本对重协商限制的支持程度不一样:
- OpenSSL 1.0.1 到 1.0.2:默认限制重协商,但可以通过配置放开;
- OpenSSL 1.1.0 及以上:
SSL_OP_NO_RENEGOTIATION可用,能彻底禁用; - OpenSSL 3.x:默认行为更严格,重协商支持进一步弱化。
所以修复前先确认你的 OpenSSL 版本,这决定了你能用哪种手段。
4. 不同服务的具体修复实操
4.1 先确认 OpenSSL 版本和当前配置
不管修哪个服务,第一步都是确认底层 OpenSSL 版本,因为很多中间件的 TLS 行为最终由 OpenSSL 决定。
# 查看 OpenSSL 版本 openssl version -a # 查看服务实际链接的 OpenSSL 库 ldd $(which nginx) | grep ssl ldd $(which httpd) | grep ssl这里有个坑:系统里可能装了多个 OpenSSL,openssl version显示的是命令行工具的版本,不代表你的服务链接的就是这个版本。一定要用ldd看服务实际链接的库。我之前就遇到过命令行显示 1.1.1,但 Nginx 链接的是 1.0.2 的情况,导致配置行为不一致。
4.2 Nginx 的修复
Nginx 从 1.11.0 开始支持ssl_renegotiation相关配置。修复方式是在server块或http块里加上:
server { listen 443 ssl; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # 禁用客户端发起的重协商 ssl_renegotiation off; # 建议同时加上这些加固项 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; }改完nginx -t测试配置,然后nginx -s reload平滑重载。
注意:
ssl_renegotiation off这个指令在部分老版本 Nginx 上不存在,如果报“unknown directive”,说明你的 Nginx 版本太老,需要升级,或者改用 OpenSSL 层面的配置。
4.3 Apache httpd 的修复
Apache 的配置项是SSLInsecureRenegotiation,默认值是off(即默认不允许不安全的重协商),但有些发行版的默认配置会打开它。修复方式:
# 在 ssl.conf 或 httpd-ssl.conf 中 SSLInsecureRenegotiation off # 如果使用较新的 mod_ssl,还可以限制协议版本 SSLProtocol -all +TLSv1.2 +TLSv1.3改完apachectl configtest检查,然后apachectl graceful重载。
这里有个细节:SSLInsecureRenegotiation off禁用的是“不安全的”重协商(即没有 RFC 5746 扩展的重协商),对于支持 RFC 5746 的客户端,重协商仍然可能被允许。如果你要彻底禁用客户端重协商,需要结合 OpenSSL 的SSL_OP_NO_RENEGOTIATION,这通常需要改代码或升级到支持该选项的版本。
4.4 Tomcat 的修复
Tomcat 的 TLS 配置在server.xml的Connector里。对于使用 JSSE 的连接器:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true"> <SSLHostConfig> <Certificate certificateKeystoreFile="conf/keystore.jks" type="RSA" /> </SSLHostConfig> </Connector>JSSE 层面可以通过系统属性限制重协商:
# 在 catalina.sh 或 setenv.sh 中添加 -Djdk.tls.rejectClientInitiatedRenegotiation=true这个属性从 JDK 8u261 左右开始支持,作用是直接拒绝客户端发起的重协商。这是 Tomcat 场景下最干净的修复方式。
提示:如果你的 Tomcat 用的是 APR 连接器(基于 OpenSSL),那就要按 OpenSSL 的方式处理,
jdk.tls.rejectClientInitiatedRenegotiation不生效。
4.5 自研服务(基于 OpenSSL)的修复
如果是自己写的 C/C++ 服务,直接调 OpenSSL API,修复方式是在SSL_CTX初始化时设置选项:
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); // OpenSSL 1.1.0 及以上 SSL_CTX_set_options(ctx, SSL_OP_NO_RENEGOTIATION); // 如果是老版本,可以用回调限制重协商次数 SSL_CTX_set_info_callback(ctx, info_cb);对于老版本 OpenSSL,可以通过info_callback统计重协商次数,超过阈值就断开连接:
void info_cb(const SSL *ssl, int where, int ret) { if (where & SSL_CB_HANDSHAKE_START) { // 统计重协商次数,超过阈值则关闭 // 具体阈值根据业务调整 } }这种方式比较土,但兼容性好,适合不能升级 OpenSSL 的老系统。
5. 修复后的验证方法
5.1 用 openssl 命令行验证
修复完不能只看配置,一定要实际验证。最直接的工具是openssl s_client:
# 连接服务 openssl s_client -connect your-server:443 -tls1_2 # 连接成功后,在交互界面输入 R 触发重协商 R如果修复成功,输入R后应该看到类似RENEGOTIATING之后报错、连接被拒绝或直接断开。如果还能正常重协商,说明修复没生效。
5.2 用扫描工具复测
如果有条件,用之前报出告警的那个扫描器复测一遍,确认告警消失。如果没有扫描器,可以用一些开源的 TLS 检测工具,重点看输出里有没有Secure Renegotiation相关的提示,以及是否接受客户端重协商。
5.3 业务回归验证
这一步最容易被忽略,但最重要。修复 TLS 配置后,一定要用真实业务客户端跑一遍完整流程:
- 浏览器访问是否正常;
- 移动端 App 的接口调用是否正常;
- 老版本 SDK 是否还能连接;
- 长连接业务是否有异常断开。
我这次修复后,就发现有一个内部工具用的是很老的 HTTP 客户端库,禁用重协商后它连不上了。后来单独给那个工具开了个兼容端口,才解决问题。
6. 常见问题与排查技巧实录
6.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 配置改了但扫描仍报 | 配置未生效/服务未重载 | 检查配置语法、确认重载成功、确认服务链接的 OpenSSL 版本 |
| 禁用后部分客户端连不上 | 客户端依赖重协商 | 抓包看握手过程,确认是否在重协商阶段失败 |
| Nginx 报 unknown directive | 版本不支持该指令 | 升级 Nginx 或改用 OpenSSL 层面配置 |
| Tomcat 属性不生效 | 用的是 APR 连接器 | 确认连接器类型,APR 需按 OpenSSL 方式处理 |
| 重协商仍被接受 | 只禁用了不安全重协商 | 需要彻底禁用客户端重协商,检查 OpenSSL 选项 |
6.2 几个我踩过的坑
坑一:只看命令行 OpenSSL 版本,没看服务实际链接的库。前面提过,这个坑导致我一开始配的选项在服务里根本不生效。后来用ldd才发现服务链接的是另一个版本的库。
坑二:以为改了配置就完事,没做业务回归。禁用重协商后,一个老工具连不上,排查了半天才发现是它依赖重协商。所以任何 TLS 配置变更,都要当成一次可能影响业务的变更来对待。
坑三:忽略了负载均衡层的配置。如果服务前面有负载均衡,TLS 可能在负载均衡层就终止了,后端服务根本看不到 TLS 流量。这种情况下,要修的是负载均衡层的配置,而不是后端服务。我这次就遇到过后端改了没效果,最后发现 TLS 是在前置层终止的。
坑四:把“禁用重协商”和“禁用 TLS 1.0/1.1”混为一谈。这是两件事。禁用老协议版本是另一套配置,虽然经常一起做,但排查问题时要分开看。
6.3 一个实用的排查思路
遇到“改了没效果”的情况,按这个顺序排查:
- 确认配置改对了地方:服务可能有多个配置文件,
include关系要理清; - 确认配置语法正确:
nginx -t、apachectl configtest这类检查一定要做; - 确认服务真的重载了:
reload不一定成功,看进程启动时间和日志; - 确认服务链接的库版本:
ldd看实际链接; - 确认 TLS 终止在哪一层:负载均衡层还是后端服务;
- 抓包看实际握手过程:
tcpdump+ Wireshark,看客户端是否真的发起了重协商。
这个顺序基本能覆盖 90% 的“改了没效果”问题。
7. 关于这个漏洞的一些个人体会
处理完这次修复,我有几个比较深的体会,分享给同样在做安全加固的同学。
第一,老漏洞不代表可以忽略,但也不代表要恐慌。CVE-2011-1473 的核心风险是 DoS,不是数据泄露。如果你的服务前面有足够的流量清洗和限流,实际风险是可控的。但既然扫描报了,该修还是要修,毕竟修复成本不高。
第二,TLS 配置变更一定要谨慎。TLS 是连接建立的第一道关,配置错了直接影响所有业务。我现在的习惯是:任何 TLS 变更,先在测试环境用真实客户端验证,再灰度到生产,观察至少一个业务周期。
第三,不要迷信“一键加固脚本”。网上有很多 TLS 加固脚本,直接套用可能把你的业务搞挂。加固的本质是“在安全和兼容之间找平衡”,这个平衡点每个业务都不一样,必须自己判断。
第四,文档和记录很重要。这次修复我详细记录了每一步的配置、验证结果、遇到的问题,后来另一个同事处理类似问题时直接参考,省了很多时间。安全加固这种事,经验沉淀比一次性修复更有价值。
最后再分享一个小技巧:如果你不确定业务是否依赖客户端重协商,可以在测试环境先开启详细日志,统计一段时间内重协商的发生次数和来源。如果次数为零,那基本可以放心禁用;如果有,就要分析这些来源是不是关键业务。这个办法比拍脑袋判断靠谱得多。