首页 / 资讯中心 / 文章详情

Spring Boot Actuator端点泄露修复实战:Nginx止血与后端加固

Spring Boot Actuator端点泄露修复实战:Nginx止血与后端加固 ★ FEATURED ARTICLE
又到攻防演练季后台告警就没停过。我这边最先被打穿的一类问题就是 Spring Boot 的 Actuator 端点泄露——扫描器一轮探测下来/actuator/env、/actuator/heapdump这些路径只要有响应漏洞单马上就飘红。说实话这个坑我这两年踩了不止一次这次专门整理一套已经在真实项目里验证过的修复方案先用 Nginx 快速止血再做 Spring Boot 侧的收敛加固整个流程从发现到确认告警消除不到一个小时就能完成。不管是开发、运维还是安全岗只要你的系统是 Spring Boot 这套技术栈这篇文章就能直接照着操作。1. 攻防演练中Actuator漏洞为什么“一打一个准”1.1 Actuator是什么为什么它总成为突破口Actuator 是 Spring Boot 自带的运维监控模块设计初衷是让开发和运维能通过 HTTP 端点查看应用健康状况、运行时指标、配置信息甚至触发一些管理操作。本身是个好东西问题出在很多人把它当成了“调试工具”图省事把端点权限全部放开没有加任何认证也没有做网络隔离等于把应用的内脏全摊开在公网上。很多老项目是从 Spring Boot 1.x 升上来的1.x 时代默认暴露的端点非常多/health、/env、/beans、/dump这些路径在根目录下就能直接访问。升级到 2.x 之后虽然默认只暴露health但不少项目在迁移时仍然沿用了原来的include: *配置或者后来因为监控需求打开了更多端点风险就这样一直延续下来。攻防演练期间红队对资产的探测基本都是自动化完成的。扫描器会在每一个 Web 端口上跑一遍常用路径字典/actuator、/actuator/health、/actuator/env、/actuator/heapdump、/actuator/mappings全在必测清单里。只要某个端点返回了正常 JSON立刻就会被标记为高风险目标随后进入人工利用环节。所以演练刚开始排在最前面的告警几乎总是 Actuator 泄露。1.2 敏感端点危害等级盘点把常见的 Actuator 端点按危害程度盘一遍我们防守的时候才知道要优先封哪个。端点路径暴露内容风险等级/actuator/env环境变量、系统属性、数据库口令、密钥高危/actuator/heapdumpJVM堆转储文件内存中的密码、Token、业务数据高危/actuator/configprops配置属性可能含第三方密钥和内部地址高危/actuator/mappings全部URL映射帮助攻击者找到隐藏接口中高危/actuator/loggers可修改日志级别可能引发信息泄露或日志轰炸中高危/actuator/beans所有Bean结构暴露组件信息中危/actuator/threaddump线程快照和调用栈中危/actuator/health健康状态可能泄露版本和组件明细低中危/env是最让人头疼的。它会把环境变量直接列出来数据库地址、账号密码、Redis 连接串、云厂商密钥、内部系统 Token经常全在配置里。配置中心下发到本机的敏感配置在这一个端点就能看光。/heapdump更直接下载下来的堆转储文件用内存分析工具打开搜一下password、secret、token这类关键字经常能翻出正在使用的真实凭证。这个点很多团队会忽略总觉得只要代码里没写死密码就安全了但 JVM 内存里存着就是存着。/mappings看起来是信息类端点实际上价值很高。它会列出 Spring MVC 注册的所有请求路径、HTTP 方法和对应的处理器攻击者拿到这份清单之后就可以照着列表找你没放在明面上的管理接口、内部白名单路径、Mock 接口直接精准打击。/loggers还有一个特殊的风险点它支持通过 POST 请求修改日志级别攻击者可以把目标包的日志级别改成 DEBUG再请求一次业务接口一旦接口打印了入参或者返回值Token、手机号、内部订单号这些敏感数据就可能被写进日志文件如果日志系统没有做脱敏等于数据变成了明文日志。1.3 防守视角下的检测手法作为防守方我们也得用攻击者的眼光来验证修复效果。最基础的检测就是直接模拟一次探测用 curl 打普通请求curl -i https://你的域名/actuator/env curl -i https://你的域名/actuator/heapdump如果返回 200 并且 body 里有 JSON说明漏洞还在如果返回 403、401、404 之类说明至少这一层已经挡掉了。注意这里说的 403 和 401 最好是由 Nginx 直接返回的如果请求穿透到了后端后端因为没暴露端点返回了 404那也只是暂时安全Nginx 层还没有形成真正的防护后续配置需要继续补。还有一个容易漏的点有些项目把 Actuator 的 base-path 改了比如/manage、/monitor。扫描器的字典里同样包含这些路径。所以修复时不要只盯着/actuator要确认清楚后端真正的管理路径到底是什么可以用配置确认也可以翻代码看网关转发规则。2. 修复方案选型为什么先上Nginx2.1 两种修复路径的取舍遇到 Actuator 泄露摆在眼前的有两个方向第一类是改 Spring Boot 应用配置只保留必要端点、加 Spring Security 认证、甚至把管理端口独立出来第二类是在入口网关用 Nginx 直接拦截/actuator相关路径。从长期看应用侧的基础加固必须要做这一点没有争议。但攻防演练期间时间非常紧实际项目中经常会遇到几种尴尬情况应用不能随便重启、服务是多实例部署的、代码权限不在你手上、运维只给了你 Nginx 的操作权限。这种情况下在 Nginx 层做拦截就是最快能落地的止血方案改配置文件 reload 一下就生效不用动 Java 进程对业务的影响几乎为零。另一个现实因素是覆盖面。很多公司有测试环境、预发环境、生产环境每个环境的 Spring 版本和配置可能都不一样挨个去改应用配置再走发布流程风险和周期都拉长不少。Nginx 在统一入口拦截配置一次就能覆盖同一入口下的所有后端服务效率是明显高的。我当时采用的策略就是“先 Nginx 止血再排期做应用侧加固”。2.2 Nginx方案的边界条件Nginx 方案有它的天然边界不能认为配完就万事大吉。它只能拦截“经过 Nginx 转发的流量”。如果攻击者能直接访问后端应用的端口比如 8080 直接对公网开放或者负载均衡规则里存在一条绕过 Nginx 指向后端 IP 的记录那你 Nginx 配得再严也无济于事。动手之前先确认三件事业务访问是否全部经过 80/443 端口进入 Nginx后端应用端口是否绑定公网网卡或者安全组是否对后端端口开放是否存在其他入口比如 CDN 回源地址指向后端、负载均衡转发规则直连后端我在实际操作中踩过一次坑。当时 Nginx 已经配好用域名去访问/actuator/env确实被 403 了但扫描器还是报漏洞。最后排查发现一条老的负载均衡转发规则把流量直接指向了应用实例的公网 IP 加端口流量完全没有经过 Nginx。后来把这条规则清理掉漏洞才真正闭环。所以确认边界不是可选动作而是 Nginx 方案生效的前提。2.3 动手前的前置确认清单把思路落成清单操作的时候不会手忙脚乱确认 Nginx 当前配置找到负责转发的 server 块和 proxy_pass 目标确认后端应用实际监听端口与 proxy_pass 指向一致确认负载均衡、防火墙、安全组策略里没有绕行通道确认监控系统对/actuator/health的依赖是否允许在一段时间内不可用记录当前 Nginx 配置文件路径和版本保证nginx -t可用这些前置工作做完后面真正写配置的时候才踏踏实实。3. 实战完整的Nginx防护配置3.1 先做一个全封闭基线最简单的修复方式是把/actuator开头的所有请求直接拒绝掉location ^~ /actuator { return 403; }这段配置的作用很直白任何以/actuator开头的 URI不管 GET 还是 POST统一返回 403。^~是告诉 Nginx一旦命中这个前缀就不要再继续匹配正则表达式了确保拦截动作干净执行。配置上去之后curl -i https://域名/actuator/env会立刻返回 403扫描器这里的漏洞告警可以先消掉。但有一个实际问题如果监控系统是通过/actuator/health做探活的这个配置会把健康检查一起干掉监控会开始报应用不可用。所以全封闭方案只适合“确认完全不需要任何 Actuator 端点”的场景或者应急时先封住再说封完再补精细规则。3.2 只放行health端点大多数生产环境需要保留健康检查这时候可以精确放行healthlocation /actuator/health { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location ^~ /actuator { return 403; }这里用location /actuator/health做精确匹配只有完整路径是/actuator/health的请求才会被转发到后端其他所有/actuator开头的请求都会落到第二个 location返回 403。有一个小细节容易踩坑精确匹配优先级最高但/actuator/health/这种带结尾斜杠的请求不会命中精确匹配而是直接落到下面的 403。如果你的监控探针带了斜杠要么改探针配置要么在 Nginx 里再补一个 location。实际生产环境中这个细节会直接导致监控探活大面积失败非常值得留意。如果还需要保留info端点可以再补一条精确匹配location /actuator/info { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location /actuator/health { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location ^~ /actuator { return 403; }这个写法的好处是简单直观缺点是端点多了以后比较繁琐。另一种紧凑的写法是利用正则location ~ ^/actuator/(health|info)$ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location /actuator { return 403; }这里必须说明 Nginx 的匹配顺序否则很容易配错。第一条是正则匹配第二条是普通前缀匹配。Nginx 会先记住最长前缀/actuator然后继续按顺序检查正则一旦正则命中就优先使用。所以/actuator/health会命中正则并转发/actuator/env不满足正则落到普通前缀匹配返回 403。如果你把第二条写成location ^~ /actuator { return 403; }那正则就永远没有机会执行因为^~命中后直接终止匹配health也会被 403 掉。这个细节我在不止一次修复中看到有人翻车写出来给各位提个醒。3.3 用IP白名单控制敏感端点如果运维人员确实需要查看某些敏感端点比如排查问题时要看/actuator/mappings、抓/actuator/heapdump做内存分析那就不能一刀切全封掉。常见的做法是保留health给监控用其他端点只允许内网运维网段访问location /actuator/health { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location ^~ /actuator { allow 10.0.0.0/8; allow 192.168.0.0/16; deny all; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }这个配置的效果是监控系统从任何网段都能访问health内网网段访问/env、/heapdump等端点会被正常转到后端公网过来的请求则一律 403。Nginx 的allow/deny是顺序匹配allow必须写在deny all前面顺序反了等于全部拒绝。这里提醒一点白名单本身依赖$remote_addr。如果流量经过了 CDN 或云负载均衡Nginx 看到的所有 source IP 都是 CDN 节点 IP白名单永远匹配不上导致内网用户也被 403。这种情况需要配合set_real_ip_from和real_ip_header去恢复真实客户端 IP我后面会专门写这个坑。如果团队习惯再加一层弱认证可以用 Nginx 的 Basic Authlocation ^~ /actuator { auth_basic Restricted; auth_basic_user_file /etc/nginx/htpasswd_actuator; allow 10.0.0.0/8; deny all; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }htpasswd文件用 Apache 的 htpasswd 命令生成即可。不过一般情况下IP 白名单已经足够Basic Auth 会在监控和自动化工具访问时增加一次认证配置成本按团队习惯取舍就好。3.4 配合独立管理端口使用如果后端配置允许还有一个更彻底的思路把 Actuator 端点独立到单独的管理端口跟业务端口完全分开。这样公网入口的 Nginx 只需要处理业务流量Actuator 根本不会暴露在外网。在 Spring Boot 的 application.yml 里这样配置management: server: port: 9091 address: 127.0.0.1 endpoints: web: base-path: /mgmt exposure: include: health,info把管理端口绑定在127.0.0.1上意味着即使安全组不小心把 9091 端口放开了外部也连不上因为端口只监听在本机回环地址。监控系统需要访问时可以通过内网代理、SSH 隧道等方式跳过去。这种方式的防护等级是最高的适合对安全要求比较严格的核心系统。独立管理端口的缺点也很明显运维习惯需要改变原有的监控采集探针、发布脚本都要增加跳板逻辑。所以它更适合作为应用侧加固里的长期优化项而不是演练期间的应急首选。3.5 配置检查和发布流程Nginx 配置改完第一步永远是测试语法语法过不了就直接把错误配置 reload 上去后果是整站不可用nginx -t nginx -s reloadnginx -t会提示语法错误和未知指令有问题先修。语法通过再执行 reloadreload 是平滑加载新配置不会中断现有连接。reload 之后立刻做一轮验证curl -i http://你的域名/actuator/env curl -i http://你的域名/actuator/health curl -i http://你的域名/actuator/heapdump正常情况应该是/actuator/env和/actuator/heapdump返回 403/actuator/health返回 200。如果启用了 IP 白名单还要从白名单网段内的机器再试一次确认影响面没有扩大。4. Spring Boot 侧加固别只靠Nginx兜底4.1 收紧端点暴露范围Nginx 能挡住外部流量但内网横向移动之后攻击者已经可能拿到某台内网主机的权限再通过内网直连后端服务时Nginx 就完全拦不到了。所以应用侧的收敛必须同步进行这是真正的兜底防线。最基础的配置是限定只暴露必要的端点management: endpoints: web: exposure: include: health,info exclude: env,beans,mappings,configprops,heapdump,threaddump,loggers,metrics,conditions,scheduledtasks,httptrace,cachesinclude白名单里只留监控系统实际需要的health和infoexclude作为双保险写出敏感端点。注意不能只靠exclude因为 Spring Boot 的处理逻辑是include优先只要 include 里没有那个端点exclude 里写不写都一样反过来如果 include 写成了*exclude 单独挡不住全部敏感端点。健康状态的展示粒度也要收敛management: endpoint: health: show-details: never如果设成alwayshealth返回的 JSON 会把数据库连接状态、磁盘空间、Redis 延迟全带出来。对攻击者来说这些信息能帮他判断后端组件类型、版本、脆弱点不值得为了图方便全暴露出去。4.2 给Actuator加上认证只依赖端点收敛还不够万一以后有人调试的时候把include又改回*防线又破了。给 Actuator 加上认证是更扎实的一层。最常见的方式是引入spring-boot-starter-security然后对 Actuator 端点做权限控制。核心思路是允许匿名访问/actuator/health因为监控探针需要其他端点要求 BASIC 认证或者限定为某个角色认证账号密码通过环境变量注入不要写死在代码里如果项目确实有特殊原因不能引入 Spring Security也可以考虑在已有网关层统一加认证过滤器但这个方案依赖架构现状不是每个项目都具备条件。实际操作中从运维角度讲类似/actuator/env这种高敏感端点建议只给极少数人开通访问权限账号要定期轮换。4.3 更换Actuator的默认路径给 Actuator 换一个不容易猜的路径也是纵深防御里的一环management: endpoints: web: base-path: /internal-monitor-2024这个办法能有效增加被自动化扫描命中的成本因为扫描器字典基本都会覆盖默认的/actuator但随机化的自定义路径很难被猜中。不过要明确一点它不是安全手段只能算“隐藏”属于降低暴露面的辅助措施不能单独作为漏洞修复手段。我在实践中把它和 Nginx 拦截、端点收敛一起使用效果是叠加的。4.4 应用侧修复后的验证改完 Spring Boot 配置需要重启应用验证方法跟之前的探测一样curl -i http://127.0.0.1:8080/actuator curl -i http://127.0.0.1:8080/actuator/env curl -i http://127.0.0.1:8080/actuator/health正常情况下/actuator根路径和/actuator/env应该返回 404 或 401/actuator/health返回 200。加上 Spring Security 之后未认证请求返回 401 也符合预期。这一步验证一定要在应用本机做不要绕 Nginx因为要确认的就是后端本身已经收紧。5. 常见问题与排查经验配置贴了还是不生效5.1 为什么返回404而不是403如果 curl/actuator/env返回的是 404同时响应体带明显的 Tomcat 或 Spring 的 404 页面说明请求已经穿过 Nginx到了后端应用层是后端自己拒绝了这个路径。这不算坏事至少端点没有暴露但从 Nginx 层的防护角度看拦截动作还没有生效。排查方法很简单看响应头。Nginx 返回的 403 响应头和 Tomcat/Jetty 返回的 404 响应头有明显区别后端容器一般会带Server: Tomcat之类的标志。如果想要 Nginx 层直接拦截检查一下当前生效的 location 配置是否真的匹配了这个路径可以用nginx -t确认语法再看 access log 里这条请求命中了哪个 location。5.2 配置了放行health但监控还是全红这是出现频率最高的一个坑原因多半是 location 组合写错了。很多人会写成location ^~ /actuator { return 403; } location /actuator/health { proxy_pass http://127.0.0.1:8080; }这个顺序下/actuator/health直接被^~ /actuator截走返回 403下面的精确匹配根本没有执行机会。Nginx 的^~一旦命中就停止后续匹配这是规则不是 Bug。正确做法是把精确放行写在前面用location /actuator/health优先匹配然后再用location ^~ /actuator做兜底或者像我上面示范的那样用正则放行加普通前缀兜底。改完以后一定要 curl 一次确认。5.3 使用了IP白名单但内网也被403最常见的原因是多级代理环境下Nginx 拿到的$remote_addr是 CDN 节点或者负载均衡器的 IP不是真实客户端 IP。allow 10.0.0.0/8判断的其实是 CDN 节点的地址自然命不中。解决办法是在 Nginx 里配置真实 IP 解析set_real_ip_from 100.64.0.0/10; set_real_ip_from 203.0.113.0/24; real_ip_header X-Forwarded-For; real_ip_recursive on;set_real_ip_from要填上游节点CDN、云负载均衡的真实 IP 段real_ip_header从 XFF 里取客户端 IPreal_ip_recursive on表示从右往左找第一个非信任 IP。配置完以后$remote_addr才会变成真实客户端地址。注意set_real_ip_from的网段一定要准确否则要么取不到真实 IP要么存在伪造风险。5.4 路径大小写和变体怎么处理Nginx 的 location 匹配默认是大小写敏感的^~ /actuator拦不住/Actuator/env扫描器也会尝试这种大小写变体。Spring Boot 默认对路径大小写敏感所以/Actuator基本不会路由到后端风险相对有限但为了保险可以改成不区分大小写的正则location /actuator/health { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location ~* ^/actuator { return 403; }精确匹配的优先级最高/actuator/health会被正确放行其他/actuator开头的路径包括大小写变体都会掉进正则拦截。这个组合既保持了监控可用又能覆盖变体绕过。5.5 前端路由和actuator路径冲突前后端分离的项目里有时候前端框架把某个路由命名成了/actuator相关路径比如前端单页应用的路由是/actuator-page如果 Nginx 用location ~* ^/actuator去做拦截会误伤正常的前端页面。这种场景下要把拦截规则做得更精确比如只匹配典型端点location ~* ^/actuator/(env|heapdump|beans|mappings|configprops|loggers|threaddump) { return 403; }或者在前端和后端之间再加一层区分根据路径前缀分别转发到静态资源和后端应用。实际操作中先确认前端路由里到底有哪些和/actuator相关的路径再决定拦截粒度和放行方式。5.6 常用排查命令汇总最后整理一份我每次排查都会用到的命令清单nginx -t nginx -s reload tail -f /var/log/nginx/access.log curl -i http://你的域名/actuator/env curl -i http://你的域名/actuator/health如果觉得 access log 看不出命中了哪个 location可以在 Nginx 配置里临时打开 debug 级别日志但这会加大日志写入量生产环境慎用排查完立刻调回去。还有一个省力的方式把整套探测写在脚本里对着候选路径一次跑完直接在终端里对比状态码。6. 一些实际操作后的体会这轮修复做下来我的核心体会是Nginx 配置只是止血工具Spring Boot 侧的收敛加固才是长期防线两边的边界必须同时守住。演练期间时间紧先用 Nginx 统一拦截确实能最快让告警消失但如果后端配置一直不收敛内网横向之后风险依然存在。安全修复不能只看外网响应要把内网访问路径也当成攻击路径来看。我现在每次演练前会固定做一轮自检先检查所有网关入口的转发规则确认没有绕过 Nginx 的直连通道再对每个子域名的/actuator系列路径做一次状态码探测把返回 200 的记录全部拉出来人工确认。这套流程跑下来基本不会漏比临时翻代码高效得多。最后再分享一个小技巧把巡检脚本放进定时任务每周跑一次一旦哪个应用又有人把端点全量打开响应码会立刻异常能在漏洞被外部扫描器发现之前先发现自己。安全这件事自己动手永远比等告警强。
阅读完成 · 觉得有帮助?
咨询建站