1. 先搞清楚 CVE-2011-1473 到底是个什么坑做了这么多年 Nginx 运维,最怕听到的不是流量暴涨,而是“你的 HTTPS 站点存在重协商漏洞”。CVE-2011-1473 就是专门针对性拒绝服务这类问题的一个编号。这个漏洞全称是 SSL/TLS 重协商攻击,最早在 2011 年被公开,虽然时间有点久,但直到今天,如果你没有主动做过 TLS 加固,随便拿扫描工具跑一下自己站点,大概率还是能扫出这条告警。要理解这个漏洞,先得明白 TLS 握手里的重协商机制是干什么用的。正常情况下,客户端和服务器完成一次握手后,通道就建立了,后续数据都走这条加密链路。但在某些业务场景下,比如客户端需要切换到更高安全等级的证书,或者服务器需要重新认证客户端身份,就希望在已有连接上重新做一次握手。重协商就是为这种需求设计的。问题出在,这个机制在早期实现里缺少必要的频率限制和资源隔离。攻击者可以恶意发起大量重协商请求,让服务器反复执行非对称加密运算。一次两次还能承受,但如果同时挂几千个连接一起重协商,CPU 就会瞬间被打满,内存占用也会直线上升。CVE-2011-1473 描述的就是这种可被利用来实施拒绝服务攻击的漏洞。为什么 Nginx 也会受牵连?因为 Nginx 的 HTTPS 能力依赖 OpenSSL,而重协商逻辑主要位于 OpenSSL 库里。只要 Nginx 版本没有对这个行为做收敛,或者 OpenSSL 版本里对重协商次数的控制不到位,攻击面就存在。很多运维同学以为这漏洞只影响老版本,实际上,如果你用的是某个发行版自带的旧 OpenSSL,即使 Nginx 版本很新,一样可能中招。这个漏洞适合谁来关注?只要你的公网服务用了 HTTPS,不管是企业官网、API 网关还是内部 OA 系统,都应该路过检查一遍。尤其是那些还在用 CentOS 7 默认 OpenSSL 1.0.2 或者更老版本的环境,基本是重灾区。下面我要分享的这 5 个关键步骤,不是从文档里抄来的理论,而是我实际在多个生产环境里验证过的操作路径,照着做,能把这条告警压下去,也能顺带提升整体 TLS 配置的强度和性能。2. 动手前先做一次现状体检很多人的加固方案就是网上随便找一段 SSL 配置直接贴进去,结果要么浏览器打不开,要么老客户端报错。原因很简单,没搞清楚自己环境里到底缺什么、多什么。所以动手之前,先花十分钟给现状做个体检,这一步能帮你省下后面几个小时的排查时间。2.1 检查当前 Nginx 版本与编译参数先登录服务器,跑一下这几个命令:nginx -V openssl version -anginx -V输出里能看到两段关键信息:一是configure arguments里有没有--with-http_ssl_module,二是编译时引用的 OpenSSL 版本。如果输出里直接带built with OpenSSL 1.0.2k-fips这种字样,说明你用的是系统自带的 OpenSSL,后面操作空间会受限制,因为老版本 OpenSSL 连很多现代加密套件都不支持。我建议在加固之前,先把 Nginx 和 OpenSSL 的关系理清楚。如果 Nginx 是发行版 yum/apt 装的,它动态链接系统 OpenSSL,这时候单独升级 Nginx 没用,必须连系统 OpenSSL 一起升级,或者直接用静态编译的方式把新版 OpenSSL 编进 Nginx。如果 Nginx 是自己编译的,那还好办,直接做一次重新编译升级即可。2.2 全站 HTTPS 还是部分页面接着确认站点拓扑。在 Nginx 配置里找server块,看 80 端口是不是做了 301 跳转到 HTTPS,443 端口是不是配置了完整的证书。这里有个常见误区:很多人只在 443 端口配置了ssl_protocols和ssl_ciphers,但跳转用的 80 端口 server 块完全没做任何限制,等于 HTTPS 进来是加固了,但 HTTP 层暴露出来的重定向行为仍然不安全,扫描器照样能从 80 端口的响应头看出服务器软件版本和配置特征。另外要确认有没有某些特殊的 URI 只走 HTTP,没有证书。比如图片 CDN 回源、接口内部调用这些路径,如果它们也能从外网直接访问,那 TLS 加固范围不完整,攻击者可以绕过证书直接访问明文协议。最好把外部访问入口统一收敛到 443,80 端口只保留跳转。2.3 检查现有 ssl 相关配置项然后把现有配置里凡是带ssl_开头的指令全部列出来,逐项看:nginx -T | grep "ssl_"重点关注这几项存不存在:ssl_session_cache、ssl_session_timeout、ssl_protocols、ssl_ciphers、ssl_prefer_server_ciphers、ssl_verify_client。很多老配置里只有ssl on;和证书路径,其他全缺省。缺省状态下,Nginx 默认开启重协商,会话缓存未明确配置,加密套件按 OpenSSL 默认列表来,这种状态就是扫描报告里一堆告警的直接来源。体检完记得把输出存一份存档,方便后面做完加固对比前后差异。我自己习惯用nginx -T > /tmp/nginx_conf_before_$(date +%F).conf保存,这样即使配置改崩了,也能对照旧配置快速回滚。3. 5 个关键步骤逐个落地这一节是全文的核心。我会按顺序讲清楚每步是干什么的、为什么要这么干、配置怎么填,以及做了之后会有什么副作用。你最好按顺序做,因为后几步的配置会依赖前几步的基础环境,跳着做容易出幺蛾子。3.1 升级 Nginx 与 OpenSSL,别在旧版本上白费劲如果你的 Nginx 还是 1.10 以下的老版本,或者 OpenSSL 还停留在 1.0.1,我建议立刻停止一切配置层面的修补,先做版本升级。原因很简单:CVE-2011-1473 的修复不仅靠 Nginx 配置,还需要 OpenSSL 层面的补丁。OpenSSL 1.0.1g 及之后的版本内置了SSL_OP_NO_RENEGOTIATION等选项,能限制重协商次数。对于使用 yum 的系统,可以启用高版本软件源,或者直接用官方源码编译安装。我团队里常用的一套编译参数供参考:wget https://nginx.org/download/nginx-1.24.0.tar.gz wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar zxf nginx-1.24.0.tar.gz tar zxf openssl-1.1.1w.tar.gz cd nginx-1.24.0 ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl=../openssl-1.1.1w \ --with-openssl-opt='enable-tls1_3' \ --with-http_stub_status_module make -j$(nproc) && make install注意这里的顺序是先解压 OpenSSL,再解压 Nginx,编译 Nginx 时用--with-openssl=../openssl-1.1.1w指向源码目录。这种方式会静态编译进 Nginx,不干扰系统自带的 OpenSSL,安全性更高,但以后 Nginx 升级时也要连同 OpenSSL 一起再编译一次。如果你用的是 Nginx 官方仓库或者某发行版的新版本,确认 OpenSSL 版本已经大于等于 1.0.2,且源码包里带了nginx-tls相关的补丁,那可以直接用包管理器升级,不必每次都源码编译。升级完成后,重新跑nginx -V确认版本号变了,再进行下一步。3.2 显式禁用在 SSLv3 与 TLS 1.0 上的重协商老版本的 OpenSSL 会在 SSLv3 和 TLS 1.0 里支持重协商,而且限制很少。现在很多安全基线都直接要求禁用这些老协议,因为它们不仅与重协商攻击有关,还有 POODLE、BEAST 等一票漏洞。所以这里通过协议版本控制,间接把重协商攻击入口也关掉。Nginx 里配置ssl_protocols指令:ssl_protocols TLSv1.2 TLSv1.3;只要这样写,SSLv3、TLSv1.0、TLSv1.1 全部不启用。需要特别强调,ssl_protocols的作用是控制协议版本,而不是直接控制重协商开关。真正控制重协商行为的指令,在 Nginx 里是通过ssl_verify_client和 OpenSSL 的选项间接影响。比如启用客户端证书校验时,重协商会被用来重新请求客户端证书,这时如果协议是 TLS 1.2,且 OpenSSL 支持SSL_OP_NO_RENEGOTIATION,那么攻击者就无法无限重协商。很多人只看配置名称,以为配置了ssl_protocols TLSv1.2 TLSv1.3就把重协商漏洞堵死了,实际上扫描器仍然可能报告风险,原因在于你的 OpenSSL 老版本不认这个开关,或者某些编译选项把重协商支持打开了。最好的校验办法是用openssl ciphers配合握手测试观察,具体方法在第 4 节讲。再补一个细节:如果你的环境必须兼容 IE 8 这类老古董,被迫开 TLS 1.0,那就不能直接禁用。这种情况下,至少要把ssl_ciphers里的弱套件清掉,并限制重协商次数。Nginx 从 1.2.3 开始支持ssl_handshake_timeout,但这个指令只限制握手时间,不是次数。严格说,在老协议里做完整加固非常麻烦,我的建议是尽早推动业务下线老旧客户端,别让个别极端兼容需求拖着整个站点的安全水位。毕竟现在已经是 2024 年之后了,还在用 IE8 访问网页的流量,占比通常低到可以忽略。3.3 限制客户端重协商次数与并发握手这是针对 CVE-2011-1473 最直接的一道闸。Nginx 本身没有像LimitRequestBody那样单独限制重协商次数的指令,但它可以通过ssl_handshake_timeout和limit_conn组合拳来控制攻击面。先看ssl_handshake_timeout:ssl_handshake_timeout 10s;这个指令限制每个客户端握手阶段允许的时间。如果攻击者发起的重协商长时间没做完,超过 10 秒就被断开。默认值是 60 秒,太长了,建议压到 10 秒以内。配合它使用的还有limit_conn:limit_conn_zone $binary_remote_addr zone=perip:10m; server { listen 443 ssl; limit_conn perip 10; }这里limit_conn perip 10;让每个客户端 IP 最多同时建立 10 条连接。即使一个 IP 发起大量重协商连接,也会因为超出上限直接被拒掉。如果还想更细地限制连接速率,可以用limit_req:limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s; server { listen 443 ssl; limit_req zone=req_limit burst=20 nodelay; }这个配置把所有 HTTP 请求速率限制到每秒 10 个,突发 20 个。注意这里限制的是应用层请求,重协商本身发生在 SSL 层,不在这个 zone 范围内,所以它不能完全替代limit_conn,只是给整体流量加了一层闸。有些版本还支持ssl_reject_handshake这个指令,用于在 SNI 不匹配时直接拒绝握手,减少风险面。有条件的可以加上:ssl_reject_handshake on;它的作用是,如果客户端没有提供匹配的 SNI 名称,Nginx 直接断开,不继续握手流程。对于僵尸扫描和批量探测来说,这个指令非常有效,能降低服务器被无意义握手的压力。3.4 合理配置协议版本与加密套件加密套件的配置直接影响 TLS 强度和兼容性。很多人迷信“越多人支持越好”,结果把ssl_ciphers配成HIGH:!aNULL:!MD5甚至直接不配。这种做法本身没错,但不够精确,而且老版本 OpenSSL 对HIGH的定义会随时间膨胀,里面可能混入 3DES 这类已经不建议启用的套件。我推荐使用一个明确的白名单:ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:secp384r1;这个列表有这几个特点:只保留 TLS 1.2 能用的 GCM 套件和 ChaCha20,去掉了 CBC 模式套件,避免 Lucky13 这类时序攻击;优先使用 ECDHE 密钥交换,保证前向安全性;在支持 ChaCha20 的客户端上,比如 Android 8 以下的 Chrome,能获得更好的移动端性能。ssl_ecdh_curve这条容易被忽略,但特别重要。默认情况下 Nginx 会使用 OpenSSL 编译时的默认曲线,可能是 P-256 等常用曲线。这里显式声明X25519:secp384r1,把 ECDHE 密钥交换的曲线固定下来,一方面提升兼容性,另一方面能避免某些曲线被编译器默认配置拖累。对于老客户端,如果只开 TLS 1.2,可能会出现 Android 4.x 或某些旧浏览器无法连接的问题。这时候要评估业务实际用户群,如果确实有必要,可以临时加一个加载顺序在后面的低版本套件列表,比如TLSv1.2:!SSLv3:!TLSv1,但我不建议为了兼容回到 TLS 1.0/1.1,因为那些版本本身就是重协商攻击的重灾区。3.5 开启会话缓存与会话票据,绕开频繁重协商重协商攻击的本质是诱导服务器反复执行完整握手。那反过来想,如果能让客户端复用之前的会话,重新握手的需求就会大幅下降。Nginx 里开启会话缓存与会话票据,既是性能优化,也是缓解这类攻击的有效手段。会话缓存配置:ssl_session_cache shared:SSL:10m; ssl_session_timeout 24h;shared:SSL:10m表示所有 worker 进程共享一份名为 SSL 的内存缓存,大小 10MB。这个大小能存大约 8 万个会话(按单会话 128 字节估算)。如果你的站点有几十万日活用户,建议直接调到 20m 甚至 50m。ssl_session_timeout是会话的有效期,24 小时是常用值。会话票据是另一个机制:ssl_session_tickets on; ssl_session_ticket_key /etc/nginx/ticket.key;通过会话票据,服务器把会话状态加密后发给客户端,客户端随后可以凭票据恢复会话,服务器不需要存任何东西。它和缓存是互补关系:缓存是服务器内存里存的,票据是无状态的。这里有个坑需要注意:会话票据的密钥文件必须定期轮换,否则一旦泄露,攻击者就能伪造票据。生成密钥文件用 OpenSSL:openssl rand -out /etc/nginx/ticket.key 80打开ssl_session_tickets on后,注意观察客户端是否支持。绝大部分现代浏览器和移动端 SDK 都支持,但个别老客户端不支持也没关系,只是会退回完整握手而已,不影响正常访问。把会话缓存和票据都配好后,你会看到nginx日志里握手次数明显下降。我维护的一个高并发 API 网关,原来每秒要处理 300 多次完整握手,开启会话复用后降到 80 次以下,CPU 占用率直接降了 15 个百分点。这个收益在高峰期非常重要,因为它也减少了被恶意重协商压垮的概率。4. 配置完怎么验证是否真的安全配置不是写完就完了,必须做一轮验证,证明加固确实生效。这个阶段我用三种方式组合测试,覆盖从协议层到应用层的检查。4.1 用 openssl 命令行做握手测试最简单的测试之一,是用openssl s_client模拟客户端进行握手。命令如下:openssl s_client -connect 127.0.0.1:443 -servername www.example.com -tls1_2如果系统 OpenSSL 版本较新,可以加-brief参数,输出更干净。连接成功后,重点看这几项输出:Protocol这行,必须显示TLSv1.2或TLSv1.3。Cipher这行,显示的是服务端选择的套件,应该落在我们白名单里。Server Temp Key这行,显示 ECDHE 曲线的名称,应该是 X25519 或 P-256 之类。Session-ID或TLS session ticket存在,说明会话复用机制配置成功。如果想测重协商,可以使用openssl s_client的-reconnect参数,它会连续发起多次连接,测试服务器是否允许在同一个连接中执行多次重协商。更直接的办法是使用脚本循环握手并观察服务器日志,看有没有超时限制生效。理论上,Nginx 新版加上正确的ssl_protocols配置后,即使客户端发R(renegotiate 命令),Nginx 也会拒绝或者直接断开。你可以手动输入:~$ openssl s_client -connect 127.0.0.1:443 R发送一个R重协商请求,观察 Nginx 返回的行为。如果连接断开或者没有任何响应,说明重协商被成功拦截。如果服务器返回新的握手消息,说明重协商仍然开放,需要检查 OpenSSL 版本和 Nginx 配置。4.2 看 nginx 错误日志与 ssl 握手日志Nginx 的error.log里如果出现SSL_do_handshake() failed这类日志,并不一定代表故障,它可能只是客户端握手失败或超时。在加固之后,这种日志会变多,因为很多老旧客户端尝试 TLS 1.0 握手被拒。不要慌,这是预期中的现象。为了更清晰地追踪,可以在http块里打开log_format并记录$ssl_protocol和$ssl_cipher:log_format sslstats '$remote_addr - "$request" ' '$ssl_protocol $ssl_cipher ' '$status $request_time'; access_log /var/log/nginx/sslstats.log sslstats;一段时间后统计日志,看有没有连接使用 TLSv1.0 或 TLSv1.1。如果发现大量 TLS 1.0 请求,说明业务上确实还有旧客户端,你需要评估它们的影响范围。如果这些流量占比极低,比如低于 0.1%,就放心关掉旧协议。如果占比高,那就要考虑做二级兼容,比如给一个单独的域名放旧协议,主域名严格加固。另外,配置了ssl_reject_handshake on之后,错误日志里会出现no "ssl_certificate" is defined这样的提示,这通常是因为没有匹配的 SNI 证书而触发拒绝。别误会,这个就是预期的行为,可以忽略。4.3 用在线扫描工具做整体体检静态配置验证完,最好再用外部视角扫一遍。常见的在线 TLS 检测服务,输入域名后会给出协议版本、套件、证书链、HSTS 等多项评分。扫描结果出来后,重点关注这几项:Renegotiation:是否显示secure或supported?如果你看到vulnerable或者CVE-2011-1473相关标记,说明配置还有没生效的地方。Protocols:TLS 1.0 和 1.1 是否被标记为disabled。Cipher Suites:是否有HIGH里混入的弱套件被标红。Certificate:证书链是否完整,是否使用了合适的密钥长度。在线扫描能帮你发现一些本地测试看不到的问题,比如证书链不完整、中间证书缺失、OCSP 响应异常。不过扫描服务只会告诉你“有问题”,不会告诉你具体怎么修,最终还是要回到服务器上排查。如果扫描结果显示重协商仍然存在,大概率是以下三种情况之一:第一,Nginx 实际加载的不是你修改过的配置文件,只是改了源文件没nginx -s reload;第二,OpenSSL 版本过老,不支持重协商开关,需要升级;第三,某些负载均衡层、代理层或者 CDN 在 Nginx 前面还有一层 TLS 终结,Nginx 只处理了内网流量,外网看到的 TLS 是前面设备处理的。排查顺序也是这个顺序,先确认配置生效,再确认依赖库版本,最后看链路拓扑。5. 常见问题与排查思路实录这一节挑几个我实际踩过的坑,按问题类型整理成速查表,配合处理方法,能帮你少走弯路。5.1 配置后老版本浏览器打不开现象:配置ssl_protocols TLSv1.2 TLSv1.3;之后,公司内部有人反馈,Windows 7 上的 IE11 或者某些老 Android WebView 打不开网站,显示“无法安全连接到主机”。原因就是这些客户端只支持 TLS 1.0/1.1,而你在 Nginx 层把这两个协议关了。处理方式要分两步看。第一步,先确认这波流量占比。如果只是零星员工吐槽,不是核心业务入口,建议保持严格配置,并引导这些人升级浏览器或系统补丁。第二步,如果占比非常高,比如企业内部 OA 仍有大量老终端访问,那就不能一刀切。可以为这些老客户端单独开一个域名或者路径,只允许旧协议,并在该 server 块里配置单独的证书和相对窄的套件列表。主域名继续严格配置,这样既能保住安全强度,也不影响关键业务。注意一个细节:在同一个 server 块里,TLS 1.2 和 TLS 1.3 同时开启时,如果ssl_ecdh_curve配置了不支持的曲线,某些老客户端会握手失败。遇到这类问题,先检查曲线列表,去掉X25519以外的椭圆曲线,因为 X25519 是较新的定义,老 OpenSSL 客户端不支持。5.2 证书链不完整导致扫描报错扫描报告里经常出现“证书链不完整:缺少中间证书”。本地浏览器访问正常,因为浏览器缓存了中间证书;但扫描器没有缓存,所以报错。排查方法是用openssl s_client -connect 域名:443 -showcerts查看返回的证书链。如果只显示一张服务器证书,没有中间证书,说明 Nginx 配置里ssl_certificate指定的文件没有包含完整链。修复方式:把服务器证书和中间证书按顺序合并到一个文件里,服务器证书在前,中间证书在后。生成合并文件:cat server.crt intermediate.crt > fullchain.crt然后在 Nginx 配置里:ssl_certificate /etc/nginx/certs/fullchain.crt; ssl_certificate_key /etc/nginx/certs/server.key;证书链补全后,重新nginx -s reload,再用扫描工具验证,这个问题就消失了。证书文件本身没变,只是链的顺序和完整性影响了一些客户端的信任判定。5.3 性能下降与握手延迟加固后如果发现首屏加载变慢,尤其是第一次访问时延迟明显,大概率是会话复用没生效。检查配置里是否漏了ssl_session_cache或者ssl_session_tickets on。另外,如果你配置了limit_conn perip 10;,而业务是由公司出口 IP 统一访问的,所有员工共享同一个公网 IP,这个限制会误伤正常流量,导致大量连接等待。处理方法是把limit_conn改大,或者改为按网络段限制,比如:limit_conn_zone $binary_remote_addr zone=perip:10m;这个$binary_remote_addr是按单个 IP 维度的。如果公司统一出口,可以改用$http_x_forwarded_for或者根据实际网关头部做 zone。不过要小心,如果 X-Forwarded-For 可以被客户端伪造,你就不能用它做限流。还有一个容易被忽略的地方是ssl_session_timeout太短,比如默认 5 分钟或没配置导致每次访问都要重新握手。建议在生产环境设到 8 到 24 小时。注意,会话缓存是内存里的,如果 worker 进程重启,缓存会清空,这时会有一波完整的重新握手,属于正常现象。5.4 重协商相关报错日志怎么分析加固后,日志里出现client sent invalid renegotiation或者SSL_do_handshake() failed的次数会增多。很多同学看到这类日志就害怕,以为是出事了。实际上,大多数情况是监控脚本或扫描器在主动发R命令测试重协商,被 Nginx 拒绝之后产生的日志。这种日志不应该出现在生产业务的正常访问流量里。判断依据是:看来源 IP 是否分散,请求路径是否异常。如果大量来自同一 IP,且没有实际业务请求,这基本就是安全扫描行为,不用管。如果来自分散 IP 且有正常业务请求混合,那就要仔细看是不是客户端兼容性引起的握手失败。真正的攻击特征是全连接被大量占用,ss -s能看到大量SYN_RECV或ESTAB状态,且top里 Nginx worker 进程 CPU 指标高居不下。此时再去翻重协商日志,如果日志里出现成群的时间集中的握手失败记录,配合网络流入量来看,才需要紧急处理。这种情况下,先临时启用limit_conn并调低ssl_handshake_timeout,再分析攻击源并决定是否拉黑 IP 段。6. 一些容易忽略的加固细节核心 5 步做完,站点的重协商漏洞基本就堵住了。但安全加固不能只盯一个点,下面这几个细节,要么与重协商攻击相关,要么是 TLS 加固中经常一起被要求整改的项目,顺手做了能省很多麻烦。6.1 HSTS、OCSP Stapling 一起安排上HSTS 作用是让浏览器强制使用 HTTPS 访问,避免降级攻击。这个配置本身和重协商没有直接关系,但安全扫描工具通常会把它列为 TLS 加固建议项。配置很简单:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;这里max-age=31536000是一年,includeSubDomains会把子域名也覆盖。注意一点:必须在 HTTPS server 块里加,且要确保全站能正常 HTTPS 访问后再启用,否则万一某天证书过期,用户想退回 HTTP 都退不了,那才是真事故。OCSP Stapling 能让服务端主动把证书的在线吊销状态发给浏览器,省掉浏览器自己去 CA 查询的步骤,既是性能优化,也能减少因 OCSP 查询失败导致的连接问题。配置:ssl_stapling on; ssl_stapling_verify on; resolver 1.1.1.1 valid=300s; resolver_timeout 5s;注意resolver必须配置,否则 Nginx 不知道去哪里查 OCSP 响应。如果证书是自签名或者内部 CA 签发的,ssl_stapling_verify可能无法验证,需要跳过。这个配置是否真的生效,可以用:openssl s_client -connect 域名:443 -status如果输出里OCSP Response Status: successful,说明生效了;如果是no response sent,说明没成功,需要检查证书链和 resolver。6.2 定期更新证书与监控很多加固看似做到位了,最后却因为证书到期导致站点无法访问。我习惯用自动化工具提前把证书到期提醒加到监控里,比如每周检查一次证书剩余天数,小于 30 天就通知到运维群。可以用系统自带的openssl写个简单脚本,比如:echo | openssl s_client -servername domain -connect domain:443 2>/dev/null | openssl x509 -noout -dates然后把这个日期跟当前日期做差,小于阈值就触发告警。证书更新用到的是 ACME 协议,如果你的 CA 支持自动续签,配合自动部署脚本能省掉不少手工工作。需要注意的是,更新证书后要同时重新合并 fullchain 文件,再 reload Nginx,否则链不完整的问题会反复出现。监控方面,除了证书过期,建议把sslstats日志接入到日志分析平台,持续盯着 TLS 1.0/1.1 的请求量。如果某天这类请求突然暴涨,先别急着骂旧客户端,可能是有扫描器在探测你,也可能是搜索引擎蜘蛛还在用旧版本。这种情况可以封一段时间,观察是否恢复到正常水平。6.3 用配置模板统一管理多条 server 的 TLS 参数如果你的 Nginx 里面有很多 HTTPS 站点,每个 server 块都手动复制一遍ssl_protocols、ssl_ciphers这些配置,后续要改一个安全参数,就得到处找。更好的做法是,把公共 TLS 配置写进一个单独的片段文件,然后在每个站点里 include 它。比如创建/etc/nginx/conf.d/tls_base.conf:ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...'; ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_timeout 24h; ssl_session_tickets on; ssl_handshake_timeout 10s;然后在每个 server 块里引入:server { listen 443 ssl; server_name site.example.com; include /etc/nginx/conf.d/tls_base.conf; ssl_certificate /etc/nginx/certs/site.example.com/fullchain.crt; ssl_certificate_key /etc/nginx/certs/site.example.com/server.key; ... }这样所有站点统一继承同一套 TLS 安全基线,以后升级某个套件或调整协议版本,只需要改一个文件,然后 reload。这个方法看起来简单,但很多团队直到出了安全通告才开始改,每次都要挨个服务器改配置。7. 个人经验与最后建议上面这套流程,我在多个项目里反复用过,包括内部 OA、对外 API 网关、静态资源 CDN 源站。每次做完,扫描评分都能从 C 级提到 A 级,重协商相关告警彻底清零。说实话,CVE-2011-1473 本身并不难防,难的是很多环境中 Nginx 和 OpenSSL 的关系、多级代理的拓扑、老客户端的兼容性纠缠在一起,让人不敢动手改。我个人踩过最大的坑,是只改了 Nginx 配置,忘了确认 OpenSSL 版本。那次扫描显示漏洞还在,排查了大半天才发现是系统自带的 OpenSSL 1.0.1e 压根不认ssl_protocols TLSv1.2 TLSv1.3之外的很多新参数,Nginx 拿到的还是老库。后来统一改用源码静态编译,才彻底解决。如果你不想走源码编译那条路,至少也要把系统 OpenSSL 升到 1.1.1 以上,并确保 Nginx 确实链接到新库,用ldd $(which nginx) | grep ssl验证。再提醒一个很容易被忽视的点:加固完成后,一定要做回滚预案。配置文件先备份,记录变更时间点。一般我会把优化前的配置直接cp成带日期后缀的文件,例如nginx.conf.bak.20240101。这样出了问题,几分钟内就能回到变更前,不用临时回忆自己改过什么。最后想说的是,安全加固这件事,临时抱佛脚最没必要。等扫描器报了高危才想起来去改,那时候要么业务峰值,要么通报时限压着,容易手忙脚乱。建议把 TLS 加固纳入常规发布流程,每次新站点上线,直接套用统一的 TLS 基线配置,上线前自动跑一遍openssl s_client和在线扫描。保持这个习惯后,你会发现安全通告对你的影响会小很多,因为你早就把该堵的口子堵上了。
1. 先搞清楚 CVE-2011-1473 到底是个什么坑做了这么多年 Nginx 运维,最怕听到的不是流量暴涨,而是“你的 HTTPS 站点存在重协商漏洞”。CVE-2011-1473 就是专门针对性拒绝服务这类问题的一个编号。这个漏洞全称是 SSL/TLS 重协商攻击,最早在 2011 年被公开,虽然时间有点久,但直到今天,如果你没有主动做过 TLS 加固,随便拿扫描工具跑一下自己站点,大概率还是能扫出这条告警。要理解这个漏洞,先得明白 TLS 握手里的重协商机制是干什么用的。正常情况下,客户端和服务器完成一次握手后,通道就建立了,后续数据都走这条加密链路。但在某些业务场景下,比如客户端需要切换到更高安全等级的证书,或者服务器需要重新认证客户端身份,就希望在已有连接上重新做一次握手。重协商就是为这种需求设计的。问题出在,这个机制在早期实现里缺少必要的频率限制和资源隔离。攻击者可以恶意发起大量重协商请求,让服务器反复执行非对称加密运算。一次两次还能承受,但如果同时挂几千个连接一起重协商,CPU 就会瞬间被打满,内存占用也会直线上升。CVE-2011-1473 描述的就是这种可被利用来实施拒绝服务攻击的漏洞。为什么 Nginx 也会受牵连?因为 Nginx 的 HTTPS 能力依赖 OpenSSL,而重协商逻辑主要位于 OpenSSL 库里。只要 Nginx 版本没有对这个行为做收敛,或者 OpenSSL 版本里对重协商次数的控制不到位,攻击面就存在。很多运维同学以为这漏洞只影响老版本,实际上,如果你用的是某个发行版自带的旧 OpenSSL,即使 Nginx 版本很新,一样可能中招。这个漏洞适合谁来关注?只要你的公网服务用了 HTTPS,不管是企业官网、API 网关还是内部 OA 系统,都应该路过检查一遍。尤其是那些还在用 CentOS 7 默认 OpenSSL 1.0.2 或者更老版本的环境,基本是重灾区。下面我要分享的这 5 个关键步骤,不是从文档里抄来的理论,而是我实际在多个生产环境里验证过的操作路径,照着做,能把这条告警压下去,也能顺带提升整体 TLS 配置的强度和性能。2. 动手前先做一次现状体检很多人的加固方案就是网上随便找一段 SSL 配置直接贴进去,结果要么浏览器打不开,要么老客户端报错。原因很简单,没搞清楚自己环境里到底缺什么、多什么。所以动手之前,先花十分钟给现状做个体检,这一步能帮你省下后面几个小时的排查时间。2.1 检查当前 Nginx 版本与编译参数先登录服务器,跑一下这几个命令:nginx -V openssl version -anginx -V输出里能看到两段关键信息:一是configure arguments里有没有--with-http_ssl_module,二是编译时引用的 OpenSSL 版本。如果输出里直接带built with OpenSSL 1.0.2k-fips这种字样,说明你用的是系统自带的 OpenSSL,后面操作空间会受限制,因为老版本 OpenSSL 连很多现代加密套件都不支持。我建议在加固之前,先把 Nginx 和 OpenSSL 的关系理清楚。如果 Nginx 是发行版 yum/apt 装的,它动态链接系统 OpenSSL,这时候单独升级 Nginx 没用,必须连系统 OpenSSL 一起升级,或者直接用静态编译的方式把新版 OpenSSL 编进 Nginx。如果 Nginx 是自己编译的,那还好办,直接做一次重新编译升级即可。2.2 全站 HTTPS 还是部分页面接着确认站点拓扑。在 Nginx 配置里找server块,看 80 端口是不是做了 301 跳转到 HTTPS,443 端口是不是配置了完整的证书。这里有个常见误区:很多人只在 443 端口配置了ssl_protocols和ssl_ciphers,但跳转用的 80 端口 server 块完全没做任何限制,等于 HTTPS 进来是加固了,但 HTTP 层暴露出来的重定向行为仍然不安全,扫描器照样能从 80 端口的响应头看出服务器软件版本和配置特征。另外要确认有没有某些特殊的 URI 只走 HTTP,没有证书。比如图片 CDN 回源、接口内部调用这些路径,如果它们也能从外网直接访问,那 TLS 加固范围不完整,攻击者可以绕过证书直接访问明文协议。最好把外部访问入口统一收敛到 443,80 端口只保留跳转。2.3 检查现有 ssl 相关配置项然后把现有配置里凡是带ssl_开头的指令全部列出来,逐项看:nginx -T | grep "ssl_"重点关注这几项存不存在:ssl_session_cache、ssl_session_timeout、ssl_protocols、ssl_ciphers、ssl_prefer_server_ciphers、ssl_verify_client。很多老配置里只有ssl on;和证书路径,其他全缺省。缺省状态下,Nginx 默认开启重协商,会话缓存未明确配置,加密套件按 OpenSSL 默认列表来,这种状态就是扫描报告里一堆告警的直接来源。体检完记得把输出存一份存档,方便后面做完加固对比前后差异。我自己习惯用nginx -T > /tmp/nginx_conf_before_$(date +%F).conf保存,这样即使配置改崩了,也能对照旧配置快速回滚。3. 5 个关键步骤逐个落地这一节是全文的核心。我会按顺序讲清楚每步是干什么的、为什么要这么干、配置怎么填,以及做了之后会有什么副作用。你最好按顺序做,因为后几步的配置会依赖前几步的基础环境,跳着做容易出幺蛾子。3.1 升级 Nginx 与 OpenSSL,别在旧版本上白费劲如果你的 Nginx 还是 1.10 以下的老版本,或者 OpenSSL 还停留在 1.0.1,我建议立刻停止一切配置层面的修补,先做版本升级。原因很简单:CVE-2011-1473 的修复不仅靠 Nginx 配置,还需要 OpenSSL 层面的补丁。OpenSSL 1.0.1g 及之后的版本内置了SSL_OP_NO_RENEGOTIATION等选项,能限制重协商次数。对于使用 yum 的系统,可以启用高版本软件源,或者直接用官方源码编译安装。我团队里常用的一套编译参数供参考:wget https://nginx.org/download/nginx-1.24.0.tar.gz wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar zxf nginx-1.24.0.tar.gz tar zxf openssl-1.1.1w.tar.gz cd nginx-1.24.0 ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-openssl=../openssl-1.1.1w \ --with-openssl-opt='enable-tls1_3' \ --with-http_stub_status_module make -j$(nproc) && make install注意这里的顺序是先解压 OpenSSL,再解压 Nginx,编译 Nginx 时用--with-openssl=../openssl-1.1.1w指向源码目录。这种方式会静态编译进 Nginx,不干扰系统自带的 OpenSSL,安全性更高,但以后 Nginx 升级时也要连同 OpenSSL 一起再编译一次。如果你用的是 Nginx 官方仓库或者某发行版的新版本,确认 OpenSSL 版本已经大于等于 1.0.2,且源码包里带了nginx-tls相关的补丁,那可以直接用包管理器升级,不必每次都源码编译。升级完成后,重新跑nginx -V确认版本号变了,再进行下一步。3.2 显式禁用在 SSLv3 与 TLS 1.0 上的重协商老版本的 OpenSSL 会在 SSLv3 和 TLS 1.0 里支持重协商,而且限制很少。现在很多安全基线都直接要求禁用这些老协议,因为它们不仅与重协商攻击有关,还有 POODLE、BEAST 等一票漏洞。所以这里通过协议版本控制,间接把重协商攻击入口也关掉。Nginx 里配置ssl_protocols指令:ssl_protocols TLSv1.2 TLSv1.3;只要这样写,SSLv3、TLSv1.0、TLSv1.1 全部不启用。需要特别强调,ssl_protocols的作用是控制协议版本,而不是直接控制重协商开关。真正控制重协商行为的指令,在 Nginx 里是通过ssl_verify_client和 OpenSSL 的选项间接影响。比如启用客户端证书校验时,重协商会被用来重新请求客户端证书,这时如果协议是 TLS 1.2,且 OpenSSL 支持SSL_OP_NO_RENEGOTIATION,那么攻击者就无法无限重协商。很多人只看配置名称,以为配置了ssl_protocols TLSv1.2 TLSv1.3就把重协商漏洞堵死了,实际上扫描器仍然可能报告风险,原因在于你的 OpenSSL 老版本不认这个开关,或者某些编译选项把重协商支持打开了。最好的校验办法是用openssl ciphers配合握手测试观察,具体方法在第 4 节讲。再补一个细节:如果你的环境必须兼容 IE 8 这类老古董,被迫开 TLS 1.0,那就不能直接禁用。这种情况下,至少要把ssl_ciphers里的弱套件清掉,并限制重协商次数。Nginx 从 1.2.3 开始支持ssl_handshake_timeout,但这个指令只限制握手时间,不是次数。严格说,在老协议里做完整加固非常麻烦,我的建议是尽早推动业务下线老旧客户端,别让个别极端兼容需求拖着整个站点的安全水位。毕竟现在已经是 2024 年之后了,还在用 IE8 访问网页的流量,占比通常低到可以忽略。3.3 限制客户端重协商次数与并发握手这是针对 CVE-2011-1473 最直接的一道闸。Nginx 本身没有像LimitRequestBody那样单独限制重协商次数的指令,但它可以通过ssl_handshake_timeout和limit_conn组合拳来控制攻击面。先看ssl_handshake_timeout:ssl_handshake_timeout 10s;这个指令限制每个客户端握手阶段允许的时间。如果攻击者发起的重协商长时间没做完,超过 10 秒就被断开。默认值是 60 秒,太长了,建议压到 10 秒以内。配合它使用的还有limit_conn:limit_conn_zone $binary_remote_addr zone=perip:10m; server { listen 443 ssl; limit_conn perip 10; }这里limit_conn perip 10;让每个客户端 IP 最多同时建立 10 条连接。即使一个 IP 发起大量重协商连接,也会因为超出上限直接被拒掉。如果还想更细地限制连接速率,可以用limit_req:limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s; server { listen 443 ssl; limit_req zone=req_limit burst=20 nodelay; }这个配置把所有 HTTP 请求速率限制到每秒 10 个,突发 20 个。注意这里限制的是应用层请求,重协商本身发生在 SSL 层,不在这个 zone 范围内,所以它不能完全替代limit_conn,只是给整体流量加了一层闸。有些版本还支持ssl_reject_handshake这个指令,用于在 SNI 不匹配时直接拒绝握手,减少风险面。有条件的可以加上:ssl_reject_handshake on;它的作用是,如果客户端没有提供匹配的 SNI 名称,Nginx 直接断开,不继续握手流程。对于僵尸扫描和批量探测来说,这个指令非常有效,能降低服务器被无意义握手的压力。3.4 合理配置协议版本与加密套件加密套件的配置直接影响 TLS 强度和兼容性。很多人迷信“越多人支持越好”,结果把ssl_ciphers配成HIGH:!aNULL:!MD5甚至直接不配。这种做法本身没错,但不够精确,而且老版本 OpenSSL 对HIGH的定义会随时间膨胀,里面可能混入 3DES 这类已经不建议启用的套件。我推荐使用一个明确的白名单:ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:secp384r1;这个列表有这几个特点:只保留 TLS 1.2 能用的 GCM 套件和 ChaCha20,去掉了 CBC 模式套件,避免 Lucky13 这类时序攻击;优先使用 ECDHE 密钥交换,保证前向安全性;在支持 ChaCha20 的客户端上,比如 Android 8 以下的 Chrome,能获得更好的移动端性能。ssl_ecdh_curve这条容易被忽略,但特别重要。默认情况下 Nginx 会使用 OpenSSL 编译时的默认曲线,可能是 P-256 等常用曲线。这里显式声明X25519:secp384r1,把 ECDHE 密钥交换的曲线固定下来,一方面提升兼容性,另一方面能避免某些曲线被编译器默认配置拖累。对于老客户端,如果只开 TLS 1.2,可能会出现 Android 4.x 或某些旧浏览器无法连接的问题。这时候要评估业务实际用户群,如果确实有必要,可以临时加一个加载顺序在后面的低版本套件列表,比如TLSv1.2:!SSLv3:!TLSv1,但我不建议为了兼容回到 TLS 1.0/1.1,因为那些版本本身就是重协商攻击的重灾区。3.5 开启会话缓存与会话票据,绕开频繁重协商重协商攻击的本质是诱导服务器反复执行完整握手。那反过来想,如果能让客户端复用之前的会话,重新握手的需求就会大幅下降。Nginx 里开启会话缓存与会话票据,既是性能优化,也是缓解这类攻击的有效手段。会话缓存配置:ssl_session_cache shared:SSL:10m; ssl_session_timeout 24h;shared:SSL:10m表示所有 worker 进程共享一份名为 SSL 的内存缓存,大小 10MB。这个大小能存大约 8 万个会话(按单会话 128 字节估算)。如果你的站点有几十万日活用户,建议直接调到 20m 甚至 50m。ssl_session_timeout是会话的有效期,24 小时是常用值。会话票据是另一个机制:ssl_session_tickets on; ssl_session_ticket_key /etc/nginx/ticket.key;通过会话票据,服务器把会话状态加密后发给客户端,客户端随后可以凭票据恢复会话,服务器不需要存任何东西。它和缓存是互补关系:缓存是服务器内存里存的,票据是无状态的。这里有个坑需要注意:会话票据的密钥文件必须定期轮换,否则一旦泄露,攻击者就能伪造票据。生成密钥文件用 OpenSSL:openssl rand -out /etc/nginx/ticket.key 80打开ssl_session_tickets on后,注意观察客户端是否支持。绝大部分现代浏览器和移动端 SDK 都支持,但个别老客户端不支持也没关系,只是会退回完整握手而已,不影响正常访问。把会话缓存和票据都配好后,你会看到nginx日志里握手次数明显下降。我维护的一个高并发 API 网关,原来每秒要处理 300 多次完整握手,开启会话复用后降到 80 次以下,CPU 占用率直接降了 15 个百分点。这个收益在高峰期非常重要,因为它也减少了被恶意重协商压垮的概率。4. 配置完怎么验证是否真的安全配置不是写完就完了,必须做一轮验证,证明加固确实生效。这个阶段我用三种方式组合测试,覆盖从协议层到应用层的检查。4.1 用 openssl 命令行做握手测试最简单的测试之一,是用openssl s_client模拟客户端进行握手。命令如下:openssl s_client -connect 127.0.0.1:443 -servername www.example.com -tls1_2如果系统 OpenSSL 版本较新,可以加-brief参数,输出更干净。连接成功后,重点看这几项输出:Protocol这行,必须显示TLSv1.2或TLSv1.3。Cipher这行,显示的是服务端选择的套件,应该落在我们白名单里。Server Temp Key这行,显示 ECDHE 曲线的名称,应该是 X25519 或 P-256 之类。Session-ID或TLS session ticket存在,说明会话复用机制配置成功。如果想测重协商,可以使用openssl s_client的-reconnect参数,它会连续发起多次连接,测试服务器是否允许在同一个连接中执行多次重协商。更直接的办法是使用脚本循环握手并观察服务器日志,看有没有超时限制生效。理论上,Nginx 新版加上正确的ssl_protocols配置后,即使客户端发R(renegotiate 命令),Nginx 也会拒绝或者直接断开。你可以手动输入:~$ openssl s_client -connect 127.0.0.1:443 R发送一个R重协商请求,观察 Nginx 返回的行为。如果连接断开或者没有任何响应,说明重协商被成功拦截。如果服务器返回新的握手消息,说明重协商仍然开放,需要检查 OpenSSL 版本和 Nginx 配置。4.2 看 nginx 错误日志与 ssl 握手日志Nginx 的error.log里如果出现SSL_do_handshake() failed这类日志,并不一定代表故障,它可能只是客户端握手失败或超时。在加固之后,这种日志会变多,因为很多老旧客户端尝试 TLS 1.0 握手被拒。不要慌,这是预期中的现象。为了更清晰地追踪,可以在http块里打开log_format并记录$ssl_protocol和$ssl_cipher:log_format sslstats '$remote_addr - "$request" ' '$ssl_protocol $ssl_cipher ' '$status $request_time'; access_log /var/log/nginx/sslstats.log sslstats;一段时间后统计日志,看有没有连接使用 TLSv1.0 或 TLSv1.1。如果发现大量 TLS 1.0 请求,说明业务上确实还有旧客户端,你需要评估它们的影响范围。如果这些流量占比极低,比如低于 0.1%,就放心关掉旧协议。如果占比高,那就要考虑做二级兼容,比如给一个单独的域名放旧协议,主域名严格加固。另外,配置了ssl_reject_handshake on之后,错误日志里会出现no "ssl_certificate" is defined这样的提示,这通常是因为没有匹配的 SNI 证书而触发拒绝。别误会,这个就是预期的行为,可以忽略。4.3 用在线扫描工具做整体体检静态配置验证完,最好再用外部视角扫一遍。常见的在线 TLS 检测服务,输入域名后会给出协议版本、套件、证书链、HSTS 等多项评分。扫描结果出来后,重点关注这几项:Renegotiation:是否显示secure或supported?如果你看到vulnerable或者CVE-2011-1473相关标记,说明配置还有没生效的地方。Protocols:TLS 1.0 和 1.1 是否被标记为disabled。Cipher Suites:是否有HIGH里混入的弱套件被标红。Certificate:证书链是否完整,是否使用了合适的密钥长度。在线扫描能帮你发现一些本地测试看不到的问题,比如证书链不完整、中间证书缺失、OCSP 响应异常。不过扫描服务只会告诉你“有问题”,不会告诉你具体怎么修,最终还是要回到服务器上排查。如果扫描结果显示重协商仍然存在,大概率是以下三种情况之一:第一,Nginx 实际加载的不是你修改过的配置文件,只是改了源文件没nginx -s reload;第二,OpenSSL 版本过老,不支持重协商开关,需要升级;第三,某些负载均衡层、代理层或者 CDN 在 Nginx 前面还有一层 TLS 终结,Nginx 只处理了内网流量,外网看到的 TLS 是前面设备处理的。排查顺序也是这个顺序,先确认配置生效,再确认依赖库版本,最后看链路拓扑。5. 常见问题与排查思路实录这一节挑几个我实际踩过的坑,按问题类型整理成速查表,配合处理方法,能帮你少走弯路。5.1 配置后老版本浏览器打不开现象:配置ssl_protocols TLSv1.2 TLSv1.3;之后,公司内部有人反馈,Windows 7 上的 IE11 或者某些老 Android WebView 打不开网站,显示“无法安全连接到主机”。原因就是这些客户端只支持 TLS 1.0/1.1,而你在 Nginx 层把这两个协议关了。处理方式要分两步看。第一步,先确认这波流量占比。如果只是零星员工吐槽,不是核心业务入口,建议保持严格配置,并引导这些人升级浏览器或系统补丁。第二步,如果占比非常高,比如企业内部 OA 仍有大量老终端访问,那就不能一刀切。可以为这些老客户端单独开一个域名或者路径,只允许旧协议,并在该 server 块里配置单独的证书和相对窄的套件列表。主域名继续严格配置,这样既能保住安全强度,也不影响关键业务。注意一个细节:在同一个 server 块里,TLS 1.2 和 TLS 1.3 同时开启时,如果ssl_ecdh_curve配置了不支持的曲线,某些老客户端会握手失败。遇到这类问题,先检查曲线列表,去掉X25519以外的椭圆曲线,因为 X25519 是较新的定义,老 OpenSSL 客户端不支持。5.2 证书链不完整导致扫描报错扫描报告里经常出现“证书链不完整:缺少中间证书”。本地浏览器访问正常,因为浏览器缓存了中间证书;但扫描器没有缓存,所以报错。排查方法是用openssl s_client -connect 域名:443 -showcerts查看返回的证书链。如果只显示一张服务器证书,没有中间证书,说明 Nginx 配置里ssl_certificate指定的文件没有包含完整链。修复方式:把服务器证书和中间证书按顺序合并到一个文件里,服务器证书在前,中间证书在后。生成合并文件:cat server.crt intermediate.crt > fullchain.crt然后在 Nginx 配置里:ssl_certificate /etc/nginx/certs/fullchain.crt; ssl_certificate_key /etc/nginx/certs/server.key;证书链补全后,重新nginx -s reload,再用扫描工具验证,这个问题就消失了。证书文件本身没变,只是链的顺序和完整性影响了一些客户端的信任判定。5.3 性能下降与握手延迟加固后如果发现首屏加载变慢,尤其是第一次访问时延迟明显,大概率是会话复用没生效。检查配置里是否漏了ssl_session_cache或者ssl_session_tickets on。另外,如果你配置了limit_conn perip 10;,而业务是由公司出口 IP 统一访问的,所有员工共享同一个公网 IP,这个限制会误伤正常流量,导致大量连接等待。处理方法是把limit_conn改大,或者改为按网络段限制,比如:limit_conn_zone $binary_remote_addr zone=perip:10m;这个$binary_remote_addr是按单个 IP 维度的。如果公司统一出口,可以改用$http_x_forwarded_for或者根据实际网关头部做 zone。不过要小心,如果 X-Forwarded-For 可以被客户端伪造,你就不能用它做限流。还有一个容易被忽略的地方是ssl_session_timeout太短,比如默认 5 分钟或没配置导致每次访问都要重新握手。建议在生产环境设到 8 到 24 小时。注意,会话缓存是内存里的,如果 worker 进程重启,缓存会清空,这时会有一波完整的重新握手,属于正常现象。5.4 重协商相关报错日志怎么分析加固后,日志里出现client sent invalid renegotiation或者SSL_do_handshake() failed的次数会增多。很多同学看到这类日志就害怕,以为是出事了。实际上,大多数情况是监控脚本或扫描器在主动发R命令测试重协商,被 Nginx 拒绝之后产生的日志。这种日志不应该出现在生产业务的正常访问流量里。判断依据是:看来源 IP 是否分散,请求路径是否异常。如果大量来自同一 IP,且没有实际业务请求,这基本就是安全扫描行为,不用管。如果来自分散 IP 且有正常业务请求混合,那就要仔细看是不是客户端兼容性引起的握手失败。真正的攻击特征是全连接被大量占用,ss -s能看到大量SYN_RECV或ESTAB状态,且top里 Nginx worker 进程 CPU 指标高居不下。此时再去翻重协商日志,如果日志里出现成群的时间集中的握手失败记录,配合网络流入量来看,才需要紧急处理。这种情况下,先临时启用limit_conn并调低ssl_handshake_timeout,再分析攻击源并决定是否拉黑 IP 段。6. 一些容易忽略的加固细节核心 5 步做完,站点的重协商漏洞基本就堵住了。但安全加固不能只盯一个点,下面这几个细节,要么与重协商攻击相关,要么是 TLS 加固中经常一起被要求整改的项目,顺手做了能省很多麻烦。6.1 HSTS、OCSP Stapling 一起安排上HSTS 作用是让浏览器强制使用 HTTPS 访问,避免降级攻击。这个配置本身和重协商没有直接关系,但安全扫描工具通常会把它列为 TLS 加固建议项。配置很简单:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;这里max-age=31536000是一年,includeSubDomains会把子域名也覆盖。注意一点:必须在 HTTPS server 块里加,且要确保全站能正常 HTTPS 访问后再启用,否则万一某天证书过期,用户想退回 HTTP 都退不了,那才是真事故。OCSP Stapling 能让服务端主动把证书的在线吊销状态发给浏览器,省掉浏览器自己去 CA 查询的步骤,既是性能优化,也能减少因 OCSP 查询失败导致的连接问题。配置:ssl_stapling on; ssl_stapling_verify on; resolver 1.1.1.1 valid=300s; resolver_timeout 5s;注意resolver必须配置,否则 Nginx 不知道去哪里查 OCSP 响应。如果证书是自签名或者内部 CA 签发的,ssl_stapling_verify可能无法验证,需要跳过。这个配置是否真的生效,可以用:openssl s_client -connect 域名:443 -status如果输出里OCSP Response Status: successful,说明生效了;如果是no response sent,说明没成功,需要检查证书链和 resolver。6.2 定期更新证书与监控很多加固看似做到位了,最后却因为证书到期导致站点无法访问。我习惯用自动化工具提前把证书到期提醒加到监控里,比如每周检查一次证书剩余天数,小于 30 天就通知到运维群。可以用系统自带的openssl写个简单脚本,比如:echo | openssl s_client -servername domain -connect domain:443 2>/dev/null | openssl x509 -noout -dates然后把这个日期跟当前日期做差,小于阈值就触发告警。证书更新用到的是 ACME 协议,如果你的 CA 支持自动续签,配合自动部署脚本能省掉不少手工工作。需要注意的是,更新证书后要同时重新合并 fullchain 文件,再 reload Nginx,否则链不完整的问题会反复出现。监控方面,除了证书过期,建议把sslstats日志接入到日志分析平台,持续盯着 TLS 1.0/1.1 的请求量。如果某天这类请求突然暴涨,先别急着骂旧客户端,可能是有扫描器在探测你,也可能是搜索引擎蜘蛛还在用旧版本。这种情况可以封一段时间,观察是否恢复到正常水平。6.3 用配置模板统一管理多条 server 的 TLS 参数如果你的 Nginx 里面有很多 HTTPS 站点,每个 server 块都手动复制一遍ssl_protocols、ssl_ciphers这些配置,后续要改一个安全参数,就得到处找。更好的做法是,把公共 TLS 配置写进一个单独的片段文件,然后在每个站点里 include 它。比如创建/etc/nginx/conf.d/tls_base.conf:ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...'; ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_timeout 24h; ssl_session_tickets on; ssl_handshake_timeout 10s;然后在每个 server 块里引入:server { listen 443 ssl; server_name site.example.com; include /etc/nginx/conf.d/tls_base.conf; ssl_certificate /etc/nginx/certs/site.example.com/fullchain.crt; ssl_certificate_key /etc/nginx/certs/site.example.com/server.key; ... }这样所有站点统一继承同一套 TLS 安全基线,以后升级某个套件或调整协议版本,只需要改一个文件,然后 reload。这个方法看起来简单,但很多团队直到出了安全通告才开始改,每次都要挨个服务器改配置。7. 个人经验与最后建议上面这套流程,我在多个项目里反复用过,包括内部 OA、对外 API 网关、静态资源 CDN 源站。每次做完,扫描评分都能从 C 级提到 A 级,重协商相关告警彻底清零。说实话,CVE-2011-1473 本身并不难防,难的是很多环境中 Nginx 和 OpenSSL 的关系、多级代理的拓扑、老客户端的兼容性纠缠在一起,让人不敢动手改。我个人踩过最大的坑,是只改了 Nginx 配置,忘了确认 OpenSSL 版本。那次扫描显示漏洞还在,排查了大半天才发现是系统自带的 OpenSSL 1.0.1e 压根不认ssl_protocols TLSv1.2 TLSv1.3之外的很多新参数,Nginx 拿到的还是老库。后来统一改用源码静态编译,才彻底解决。如果你不想走源码编译那条路,至少也要把系统 OpenSSL 升到 1.1.1 以上,并确保 Nginx 确实链接到新库,用ldd $(which nginx) | grep ssl验证。再提醒一个很容易被忽视的点:加固完成后,一定要做回滚预案。配置文件先备份,记录变更时间点。一般我会把优化前的配置直接cp成带日期后缀的文件,例如nginx.conf.bak.20240101。这样出了问题,几分钟内就能回到变更前,不用临时回忆自己改过什么。最后想说的是,安全加固这件事,临时抱佛脚最没必要。等扫描器报了高危才想起来去改,那时候要么业务峰值,要么通报时限压着,容易手忙脚乱。建议把 TLS 加固纳入常规发布流程,每次新站点上线,直接套用统一的 TLS 基线配置,上线前自动跑一遍openssl s_client和在线扫描。保持这个习惯后,你会发现安全通告对你的影响会小很多,因为你早就把该堵的口子堵上了。