1. 项目概述当CDN“隐身衣”失效你的源站IP还安全吗最近在排查一个线上服务的问题时我遇到了一个典型的场景一个部署在云服务器上的Web应用为了提升访问速度和安全性前面套了一层CDN。理论上用户访问的是CDN的节点IP源站的真实IP被很好地隐藏了起来。但一次偶然的配置失误让我发现通过某些特定方式源站IP竟然直接暴露在了公网上。这就像给自家大门上了一把复杂的锁却忘了把后院的窗户关上。一旦源站IP被恶意扫描器或攻击者发现DDoS攻击、漏洞扫描、恶意爬虫就会绕过CDN的防护直接冲击你的服务器轻则服务不稳定重则可能导致数据泄露甚至服务器被攻陷。这个问题的核心在于即使配置了CDN如果你的源站服务器比如Nginx仍然在443HTTPS和80HTTP端口上对全世界“有求必应”那么攻击者完全可以通过扫描全网IP段直接与你的源站建立SSL/TLS握手从而获取到证书信息进而反向解析出你的域名确认这就是CDN背后的真实服务器。Nginx 1.25.5版本引入的ssl_reject_handshake指令就是为了彻底堵上这扇“窗户”而生的。它允许我们在Nginx层面直接拒绝非预期的SSL/TLS握手请求让那些试图直接连接源站的探测行为吃个“闭门羹”。这篇文章我就来手把手拆解这个风险并详细演示如何利用这个特性为你的源站穿上真正的“隐形斗篷”。2. 核心原理SSL/TLS握手与IP泄露的攻防逻辑要理解ssl_reject_handshake为何有效我们得先搞清楚攻击者是如何发现你源站IP的以及传统的防护手段为何存在漏洞。2.1 源站IP是如何被“扒出来”的假设你的域名是www.example.com使用了某CDN服务。正常的访问流程是用户解析www.example.com- 得到CDN的节点IP - 与CDN节点通信 - CDN节点回源到你的真实服务器。在这个过程中你的服务器IP例如192.0.2.100对最终用户是不可见的。然而攻击者有多种手段可以绕过这个流程全网段扫描攻击者利用工具对云服务商常用的IP段进行大规模扫描探测开放了443或80端口的服务器。发起SSL/TLS握手当扫描到你的服务器IP192.0.2.100并发现443端口开放时攻击者会像普通浏览器一样发起一个SSL/TLS握手请求。获取服务器证书你的Nginx服务器会照常返回其配置的SSL证书。这个证书里包含了一个关键信息证书的通用名称CN或主题备用名称SAN这里面很可能就写着你的域名www.example.com。关联确认攻击者将IP192.0.2.100与域名www.example.com关联起来从而确认这就是CDN背后的源站IP。这个过程不依赖于任何漏洞只是利用了HTTP/HTTPS协议的标准行为。你的服务器“太有礼貌了”对所有连接请求都给予了回应。2.2 传统防护手段的局限性在ssl_reject_handshake出现之前我们通常用以下几种方法来缓解这个问题防火墙白名单在服务器防火墙或安全组上只允许CDN提供商的IP段访问443/80端口。这是最有效的方法但维护麻烦CDN节点IP可能会变动且如果你的服务器还有其他需要公网访问的服务如API规则会变得复杂。修改默认端口将Web服务监听在非标准的端口如8443。这属于“安全通过隐匿”一旦端口被扫描到风险依旧。使用“假证书”或自签名证书配置一个与真实域名无关的证书。这样即使握手成功攻击者拿到的证书信息也无法关联到你的真实业务域名。但配置略显繁琐且需要管理多套证书。HTTP层面校验在Nginx的HTTP处理阶段通过判断$host或自定义Header如CDN回源时带的X-Forwarded-Host来决定是否处理请求。但这已经晚了因为SSL/TLS握手在HTTP协议解析之前就已经完成服务器证书信息已经泄露。ssl_reject_handshake指令的强大之处在于它将防护动作提前到了SSL/TLS握手阶段。它可以在Nginx收到Client Hello报文握手的第一个报文后不进行后续的证书交换等步骤直接断开连接从而从根本上阻止了证书信息的泄露。3. 环境准备与Nginx配置规划在开始动手之前我们需要明确目标和准备好环境。我们的目标是让源站Nginx仅接受来自可信渠道CDN的连接请求对其它直接发起的SSL握手请求予以拒绝。3.1 系统与Nginx版本确认首先确保你的Nginx版本 1.19.4。ssl_reject_handshake指令是在这个版本中引入的。我们推荐使用1.25.5或更高版本以获取更好的稳定性和功能支持。# 查看Nginx版本 nginx -v如果你的版本过低需要计划升级。对于生产环境建议先在测试环境验证。升级Nginx是一个需要谨慎操作的过程涉及编译或包管理器的更新。3.2 网络拓扑与流量分析规划你的Nginx配置前先理清流量路径正常用户流量用户 - CDN边缘节点 - CDN回源 - 你的源站Nginx。CDN回源流量这部分流量是我们要放行的。关键是如何识别它通常有两种方式IP白名单CDN服务商会提供其所有回源节点的IP地址列表。这是最可靠的标识。自定义Header大多数CDN支持在回源请求中添加特定的Header例如X-Forwarded-For,X-Real-IP, 或者一个你自定义的Secret Header如X-Origin-Verify: your_secret_token。注意ssl_reject_handshake工作在SSL层无法读取HTTP Header。因此IP白名单是与之配合使用的主要方式自定义Header可用于后续HTTP请求的二次校验。我们的配置策略将分为两层SSL层防护本方案核心利用ssl_reject_handshake对非白名单IP的SSL握手进行拒绝。HTTP层加固利用allow/deny或if判断自定义Header对侥幸到达HTTP阶段的非法请求进行拦截。3.3 获取并整理CDN回源IP白名单这是最关键的一步。登录你使用的CDN服务商的管理控制台查找“回源设置”、“节点IP”或“IP白名单”等相关文档。例如Cloudflare提供了公开的IPv4和IPv6地址列表。阿里云CDN/腾讯云CDN在控制台内有专门的回源节点IP段列表可供下载。其他厂商通常在其帮助文档或工单支持中获取。将获取到的IP段整理成CIDR格式如192.0.2.0/24并保存到一个文件中例如/etc/nginx/conf.d/cdn_whitelist.conf。这个文件我们将通过include指令在Nginx配置中引用。注意CDN的IP列表可能会不定期更新。你需要建立一个定期如每月检查并更新此白名单的机制否则可能导致CDN无法回源网站瘫痪。4. 核心配置实战部署ssl_reject_handshake接下来我们进入具体的配置环节。假设你的源站服务器上只有一个主要的Web业务通过CDN提供服务。4.1 基础配置结构我们会在nginx.conf的http块内或者在一个独立的server配置文件中进行修改。核心思路是为默认的SSL server块设置ssl_reject_handshake on;使其拒绝所有握手同时为来自CDN IP的请求创建一个特定的server块正常处理SSL握手和业务。首先创建或编辑你的主业务配置文件例如/etc/nginx/conf.d/my_website.conf。# 首先定义一个存放CDN IP白名单的变量映射块。 # 这一步需要在http块内最顶部附近定义确保后续配置能访问到。 # 我们使用ngx_http_geo_module模块它比直接在location里写allow/deny更高效。 http { # ... 其他http全局配置 ... # 定义CDN IP白名单变量 $cdn_whitelist # 默认值为0当客户端IP在白名单内时值为1。 geo $cdn_whitelist { default 0; # 在这里以CIDR格式填入你的CDN回源IP段 # 例如 Cloudflare 的部分IP请务必使用官方最新列表 103.21.244.0/22 1; 103.22.200.0/22 1; 103.31.4.0/22 1; 104.16.0.0/13 1; 108.162.192.0/18 1; # ... 更多CDN IP段 ... # 也可以使用include指令引入外部文件便于管理 # include /etc/nginx/conf.d/cdn_ips.conf; } # ... 其他配置 ... }4.2 配置“拒绝一切”的默认SSL Server这个server块监听443端口但它的唯一使命就是拒绝非预期的SSL握手。它不关联任何具体的域名证书。server { listen 443 ssl default_server; # 关键default_server 表示捕获所有未匹配其他server块的443请求 listen [::]:443 ssl default_server; # 核心指令拒绝所有SSL握手 ssl_reject_handshake on; # 可选的可以记录一下这些被拒绝的连接日志用于监控扫描行为 access_log /var/log/nginx/ssl_reject.log; # 为了节省资源错误日志可以不用记录这些握手错误 # error_log /var/log/nginx/ssl_reject_error.log; # 不需要配置 ssl_certificate 和 ssl_certificate_key # 因为握手已被拒绝根本不会走到证书验证那一步。 }配置解析listen 443 ssl default_server;default_server参数至关重要。它确保所有发往本机443端口、但未匹配到其他更具体server_name的请求都会落入这个server块处理。ssl_reject_handshake on;这就是我们的“守门神”。当Nginx收到Client Hello后直接返回一个TLS Alert报文然后关闭连接不会暴露任何证书信息。日志开启访问日志可以帮助你观察有多少扫描请求被拦截但生产环境下如果扫描量巨大可能会产生大量日志需权衡。4.3 配置“放行CDN”的业务SSL Server这个server块才是真正处理你网站业务的。它通过server_name匹配域名并且我们通过if指令和前面定义的$cdn_whitelist变量确保只处理来自CDN白名单的请求。server { listen 443 ssl; listen [::]:443 ssl; server_name www.example.com example.com; # 你的真实域名 # 关键校验仅允许CDN回源IP访问 if ($cdn_whitelist 0) { # 如果IP不在白名单返回403错误。也可以直接return 444立即关闭连接。 return 403; # 或者更彻底地return 444; # 444是Nginx特有的非标准状态码表示关闭连接且不发送响应头。 } # 你的真实SSL证书配置 ssl_certificate /path/to/your/fullchain.pem; ssl_certificate_key /path/to/your/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # ... 其他SSL优化配置 ... # 你的网站根目录和其他业务配置 root /var/www/example.com; index index.html index.htm; location / { try_files $uri $uri/ 404; } # 如果CDN回源带有特定Header可以在这里做进一步校验作为双重保险 # 例如设置一个回源密钥 # if ($http_x_origin_secret ! your_long_secret_string) { # return 403; # } access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; }配置解析if ($cdn_whitelist 0)这是一个访问控制检查。$cdn_whitelist变量在我们之前的geo块中定义。如果客户端IP不在白名单内变量值为0则直接返回403禁止访问。这个检查发生在SSL握手之后HTTP请求解析之前。它和前面的ssl_reject_handshake形成了双重防护恶意IP连SSL握手都过不了万一有漏网之鱼比如CDN IP列表更新不及时误加了某个IP在HTTP阶段还会被拦截。证书配置这里配置你域名真实的证书。因为只有CDN的请求能到达这里所以证书信息不会泄露给扫描器。双重校验可选注释部分展示了如何通过校验CDN回源时添加的自定义Header如X-Origin-Secret来增加一层安全。这要求你在CDN控制台能够配置回源Header。4.4 配置HTTP80端口的防护攻击者也可能扫描80端口。虽然HTTP没有证书泄露风险但直接暴露源站IP和业务同样危险。我们应该将80端口的所有请求重定向到HTTPS并且同样只对CDN IP放行或者直接关闭80端口的公网访问如果业务允许。# 方案一仅对CDN IP开放80端口并重定向 server { listen 80; listen [::]:80; server_name www.example.com example.com; if ($cdn_whitelist 0) { return 403; # 或 444 } # 只允许来自CDN的HTTP请求并将其重定向到HTTPSCDN回源通常也用HTTPS所以这个可能用不到 # 或者直接返回444强制CDN回源使用HTTPS return 444; } # 方案二更推荐在防火墙层面直接禁用公网对80端口的访问只允许CDN IP段。 # 这样Nginx甚至不需要监听公网80端口更安全。5. 配置测试、验证与上线流程配置完成后绝对不能直接重启生产环境的Nginx。必须经过严格的测试。5.1 语法检查与配置重载# 1. 检查配置文件语法是否正确 sudo nginx -t # 如果输出 syntax is ok 和 test is successful则进行下一步。 # 2. 平滑重载Nginx配置不影响已有连接 sudo nginx -s reload5.2 多维度验证测试测试需要从不同的源IP发起模拟攻击者和CDN。从非CDN IP测试模拟攻击者使用你的个人电脑非VPN非公司网络确保IP不在白名单访问https://你的源站IP。预期结果连接立即失败。浏览器可能显示“连接被重置”、“SSL握手错误”或“无法连接到网站”。使用curl -v https://你的源站IP命令你会看到在Client Hello发送后连接被服务器关闭没有收到证书。curl -v https://192.0.2.100 * Trying 192.0.2.100:443... * Connected to 192.0.2.100 (192.0.2.100) port 443 (#0) * TLSv1.3 (OUT), TLS handshake, Client hello (1): * Recv failure: Connection reset by peer * Closing connection 0 curl: (56) Recv failure: Connection reset by peer检查之前配置的ssl_reject.log应该能看到这条访问记录。从CDN IP测试模拟正常回源这是最关键的测试。你需要一个位于CDN回源IP段的测试服务器或者利用CDN服务商提供的“回源Host头设置”或“调试模式”让CDN节点直接访问你的源站IP进行测试。访问https://你的源站IP并在请求头中带上你的业务域名Host头。预期结果能够正常访问网站内容。因为你的测试IP在白名单内且请求匹配了server_name。也可以使用curl指定Host头测试curl -H Host: www.example.com https://192.0.2.100预期结果返回你的网站首页HTML内容。通过CDN域名访问测试真实用户场景最终通过你的域名www.example.com访问网站。预期结果网站完全正常访问速度、功能无任何影响。因为流量走了用户 - CDN - 源站的正常路径。5.3 监控与日志分析上线后需要持续监控一段时间检查业务访问日志确保没有因白名单遗漏导致CDN回源失败返回403/444错误。检查ssl_reject.log观察被拦截的扫描频率和来源IP这可以直观反映你之前暴露的风险有多大。监控服务器资源确保新的配置没有引入性能问题。6. 常见问题、故障排查与进阶技巧在实际部署中你可能会遇到以下问题6.1 CDN回源失败报403错误这是最常见的问题意味着你的CDN回源IP不在Nginx配置的白名单中。排查步骤核对IP在源站Nginx的访问日志中找到CDN回源失败的请求记录下客户端IP。检查白名单确认该IP是否在你从CDN服务商获取的最新IP段列表内。检查geo配置检查/etc/nginx/nginx.conf中geo $cdn_whitelist块的语法确保IP段格式正确CIDR。检查变量值可以在Nginx配置中临时添加调试日志打印$remote_addr和$cdn_whitelist的值。location / { # 临时调试 add_header X-Debug-Client-IP $remote_addr always; add_header X-Debug-Whitelist $cdn_whitelist always; # ... 其他配置 }更新白名单如果确认是IP遗漏立即更新白名单文件或geo块并nginx -s reload。实操心得建议将CDN IP白名单维护在一个独立的配置文件中如/etc/nginx/conf.d/cdn-whitelist.conf然后在geo块中使用include。这样更新时只需修改这个文件清晰且不易出错。同时在CDN服务商控制台设置告警当回源失败率升高时能及时通知。6.2 配置重载后Nginx报错nginx: [emerg] invalid parameter这通常是因为ssl_reject_handshake指令放在了不支持它的上下文中或者与某些指令冲突。确保上下文正确ssl_reject_handshake只能用于server块内并且该server块必须配置了listen ... ssl;。检查冲突指令在启用了ssl_reject_handshake on;的server块中不能同时配置ssl_certificate和ssl_certificate_key。因为拒绝握手根本用不到证书。检查版本再次确认Nginx版本是否支持该指令。6.3 如何应对CDN节点IP频繁变动这是运维上的一个挑战。自动化编写脚本定期如每天从CDN厂商的API或公开地址列表拉取最新的IP段与现有白名单对比如有变化则自动更新Nginx配置文件并重载。这是最理想的解决方案。使用动态DNS或网络层白名单一些云服务商的安全组或防火墙支持将“安全组”或“地址组”作为规则源你可以将CDN的IP段维护在地址组中然后在服务器安全组规则中引用该地址组。这样IP更新只需要在地址组一处修改。放宽限制不推荐如果CDN厂商无法提供稳定的IP列表可以考虑在Nginx的HTTP层使用动态验证如验证特定的回源Header密钥而在SSL层则无法做到动态放行。这种情况下可能需要权衡风险或者考虑更换CDN服务商。6.4 除了CDN还有其他可信回源渠道怎么办如果你的服务器还需要被其他自动化系统、监控平台或合作伙伴直接访问。扩展白名单将这些系统的固定出口IP也加入到geo $cdn_whitelist白名单中。使用SNI分流高级如果你有多个业务且需要被不同的客户端直接访问可以利用ssl_reject_handshake的精细化控制。为每个需要直接访问的业务配置一个独立的server块指定其server_name和证书并且不要在这个server块里设置ssl_reject_handshake on或IP限制。而将“拒绝一切”的default server作为兜底。这样只有携带了正确SNIServer Name Indication的握手请求才能被对应server处理其他一概拒绝。这要求攻击者不仅要知道你的IP还要猜对你的域名安全性更高。6.5 性能影响评估ssl_reject_handshake on指令本身开销极低它只是在TCP连接建立后、SSL握手初期就做出拒绝决定比完成完整的TLS握手并加载证书要节省大量CPU资源。因此启用此功能实际上有助于减轻服务器负载因为它提前拒绝了无效的、恶意的连接尝试。主要的性能考量点在于geo模块的IP匹配对于经过优化的Nginx来说处理一个CIDR列表的性能开销在可接受范围内。部署完这套组合拳你的源站服务器就从“礼貌的接待员”变成了“警惕的安检员”。它只对持有“VIP通行证”CDN IP的客人微笑服务对于其他任何试图直接敲门的访客都在第一时间坚决地拒之门外。这极大地缩小了攻击面是构建纵深防御体系中非常有效的一环。记住安全没有一劳永逸定期更新你的CDN IP白名单并持续监控异常日志才能让这层防护持续生效。