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

自托管Sentry SSRF风险排查:彻底关闭Source Code Scraping的配置实践

自托管Sentry SSRF风险排查:彻底关闭Source Code Scraping的配置实践 ★ FEATURED ARTICLE
前阵子给自托管Sentry做安全加固扫描报告里躺着一个高危的SSRF告警。顺着调用链排查最后发现风险来自source code scraping这个默认开启的功能Sentry后台会根据上报事件的堆栈信息自动去外部代码仓库拉取对应源码而其中不少参数是可以被上报者控制的。这篇不是Sentry入门教学而是关掉这个功能时踩到的各种坑以及我最后确定的可靠配置路径。内容主要面向自托管Sentry的运维、安全和后端开发者。全文不会给你一个“点一下按钮就好”的答案因为真实环境里这个功能由好几层配置控制少关任何一层都可能让安全测试在几天之后重新打出SSRF告警。文章里所有的命令和配置都以我在实际环境里验证过的方式为基础变量名如果有出入请优先以你手上那个版本的配置项为准。1. 问题复盘Source Code Scraping 为什么会有 SSRF 风险1.1 先理解 source code scraping 在错误链路里扮演什么角色Sentry 的价值在于把杂乱日志变成带堆栈、带上下文、带用户影响的错误事件。要做到这一点后台会保存异常发生时的堆栈帧里面包含文件名、行号、函数名等字段。默认情况下Sentry 还会尝试根据这些信息去“补全”源码上下文也就是 source code scraping。它可以读取服务端配置的仓库地址、版本控制系统集成甚至从已配置的远程代码托管平台拉取错误堆栈对应文件的内容然后在事件详情页直接展示“这一行周围长什么样”。这个功能对开发者体验提升非常明显收到告警时不用切到 Git 或 CI 工具直接在 Sentry 页面上就能看到报错代码上下文。但对自托管部署来说这个功能会把“代码仓库访问凭据”和“发起外部请求的能力”同时暴露给一个网络入口。后台 worker 拿到堆栈信息后会向外发起请求请求的目的地有一部分来自事件内容。换句话说这相当于让一个可信后台服务带着内网身份去访问攻击者给的地址。很多团队部署完 Sentry 后不会主动去动这个功能因为它在大多数正常日志场景下不会出问题。但只要 DSN 泄漏、上报入口未加防护攻击者就有机会构造特殊事件触发后台请求。这也是为什么安全扫描会把它直接标记为 SSRF 风险项。1.2 SSRF 攻击路径到底是怎么打通的SSRF 的全称是服务端请求伪造。攻击者未必能直接控制 Sentry 服务器但他能控制上报给 Sentry 的事件内容。source code scraping 的触发链路上filename、abs_path、仓库地址这些字段都是由客户端 SDK 上报后由后台读取的。如果后台不对这些字段做严格白名单和协议过滤攻击者把filename填成http://内网地址/或者file:///etc/shadow后台 worker 就可能按这个地址发起请求。攻击者不需要登录 Sentry只要能向上报接口发送合法 DSN 请求即可。他会构造一个只包含单个堆栈帧的事件帧里的文件名指向他想要探测的内网地址比如云平台元数据服务、Redis 端口、数据库管理端口。Sentry 后台去读取“源码”时实际上就是在帮攻击者做端口探测和内网访问。如果被访问的服务本身有未授权访问问题影响会立刻被放大。我用一个生活化类比给你们感受一下这就像公司前台小哥收到一个匿名邮件说你有一份文件放在某个仓库让他去取。仓库位置其实是攻击者指定的银行金库。小哥开着公司的车、用着公司地图导航过去了。前台小哥的本职工作只是取文件但路线完全被人控制问题不在小哥干活不认真而是他没有任何“只允许去官方仓库”的约束。1.3 这个坑为什么踩得特别难受我当时接到告警后第一反应是去改环境变量把开关设成关闭。改完刷新 Sentry 页面确实看不到源码上下文了心想应该解决了。但三天后扫描报告再出SSRF 依然存在。后来才发现我只改了 web 容器里的环境变量真正执行抓取动作的是 worker 容器而且 Sentry 的配置存在缓存层单纯重启一个服务根本不生效。加上我们当时还保留了一个代码托管平台的集成授权后台定时同步任务会拿着旧 token 继续向外请求。这些情况连在一起就成了“配置了但好像又没完全配置”的经典翻车现场。所以我想把整个排查和修复过程整理出来给同样自托管 Sentry 的团队一条可以抄作业的路。2. 关闭方案选型环境变量、配置文件与 UI 开关的三重考量2.1 三种常见关闭入口对比关闭 source code scraping 不是一个开关搞定的事。在真实部署里至少有三种入口可以控制配置入口作用范围适用场景主要注意事项环境变量.env或容器环境全局生效影响所有项目自托管、基础设施即代码管理变量名随版本变化布尔值千万别加引号Python 配置文件sentry.conf.py全局生效可配合复杂逻辑需要统一管理的私有化部署某些发行版初始化时会覆盖该文件Sentry 后台 UI 设置组织级或项目级快速停工、SaaS 多租户场景升级后可能被默认值覆盖审计留痕较弱环境变量适合做全局控制因为它会出现在容器编排文件中做变更审查和回滚都比较方便。Python 配置文件适合做更细致的条件判断比如只有生产环境才关闭、测试环境保留。UI 开关则适合运营同学或安全同学先快速止损但它不能被当成唯一防线因为页面上的选项不一定和后台 worker 的运行时配置完全同步。我在实际环境里最终选择的是“环境变量 UI 双闭环”。环境变量保证 worker 启动时就不具备抓取能力UI 设置保证页面层面对用户呈现出来的是“已关闭”。两者同步关闭后就算后面有人误改 UI后台 worker 依然没有能力发起抓取请求。2.2 为什么我不建议只关 UI 开关只关 UI 开关表面上很快但有两个隐患。第一自托管 Sentry 的升级脚本在拉新版本之后有可能重新生成默认配置某些版本里该功能默认开启升级后 UI 设置会被顶掉。第二UI 开关一般控制的是业务侧功能入口如果后台的集成配置仍然保留着远程仓库凭据定时任务依然有可能使用旧 token 发起请求。我们不只一次见过“UI 显示关了抓包还有外呼”的情况。环境变量则不同。它属于启动时注入的运行时配置只要部署编排文件没变进程起来就是这个状态。我们在 Kubernetes 里会把配置放到 ConfigMap 中版本变化时由 CI 流程重新渲染任何人工改动都会被下次发布覆盖。这样至少能保证配置是可审计、可版本化的。2.3 动手前先确认版本和真实配置项名称不同版本的 Sentry 对这一个功能的命名不太一样。我经历过的就有SENTRY_SOURCE_FETCH_ENABLED、SENTRY_SOURCE_SCRAPE_ENABLED还有 feature flag 形式的organizations:source-fetch。它们做的事情类似但生效位置可能不同。如果你直接照着网上的某一个变量名去配置大概率会碰壁。我自己的做法是先把 Sentry 容器里的环境变量和SENTRY_FEATURES都打印出来再到源码里搜source-fetch或source关键字。这样能快速确认你那个版本到底读的是哪个开关。另外先说一个容易混淆的点关闭 source code scraping 不等于关闭 sourcemap 解析。前者是后台抓取已发布仓库的源码来补充堆栈上下文后者是处理前端 JS 构建产物的 source map用于还原压缩混淆前的代码。很多同事会把这个搞混关了 scraping 之后跑来问为什么前端报错信息少了。两个功能作用对象不同要分开看。3. 实操自托管 Sentry 下关闭 source code scraping 的完整过程3.1 第一步确认当前开启状态留个底再改开始配置前最好先确认功能确实处于活跃状态并且把这个状态记录下来方便后面对比。最直接的办法是去 Sentry 后台看项目设置里的安全与隐私相关选项看有没有“Allow source code fetching”之类的勾选。但这个入口并不是所有版本都有没有也不必着急有更可靠的验证方式。我习惯登录到 worker 容器里抓一段出网流量分析它是否在访问外部代码仓库。命令大致是这样docker exec -it sentry-worker tcpdump -i eth0 -nn tcp and dst port 443 -w /tmp/sentry_egress.pcap等上十分钟左右把 pcap 拉出来用抓包工具看一下。如果看到raw.githubusercontent.com或代码托管平台的 API 地址说明 source code scraping 或相关同步任务正在工作。这个步骤能帮你确认到底是谁在出网也方便后续验证关闭效果。留意一个细节抓包文件不要一直放在容器临时目录记得及时转移避免撑爆容器磁盘。除了抓包还可以直接看 worker 日志。搜索source、fetch、fetch_url这类关键字通常能看到它尝试下载源码的记录。3.2 第二步修改全局环境变量并强制重建容器以 Docker Compose 部署的自托管 Sentry 为例我会在.env文件里加一行SENTRY_SOURCE_FETCH_ENABLEDfalse注意变量名不同版本可能不一样我这里写的是我实际生效的版本。如果看到自己的 Sentry 没有这个变量名就回溯一下 feature flag 的配置方式。对应的 Python 配置文件里还可以加这样一行SENTRY_FEATURES[organizations:source-fetch] False写完.env之后不能只做docker compose restart因为环境变量的变更需要在容器重建时才会被读取。正确做法是强制重建docker compose up -d --force-recreate这个命令会重新创建容器而不是简单重启。如果之前有过复杂缓存问题更稳妥的方法是先停掉所有服务docker compose down docker compose up -d但要注意down会顺带把网络和卷的引用关系都清理掉执行前确认你清楚当前部署状态。我在这个阶段踩过最典型的坑是环境变量确实加上了但我用的容器编排文件里有个变量名拼写错误导致 Sentry 根本没读到。排查时不要只看宿主机的.env要进容器里env看一下运行时实际值。3.3 第三步收掉 UI 设置与代码托管平台集成环境变量改完后还要把页面上的开关和外部集成权限一起收掉。进入 Sentry 后台找到组织设置里的安全与隐私选项把和 source fetching 相关的开关取消勾选。如果项目级设置继承自组织级需要确认每个项目都没有被单独覆盖成开启状态。如果项目数量很多可以走 API 批量拉取项目清单再逐个检查。我当时写了个脚本逻辑很简单循环所有项目查询该项目的安全设置数据如果发现source相关字段为开启就把值更新为关闭。这类操作建议先在测试项目上验证避免误改全部项目。然后进入集成管理页把不需要的代码托管平台安装停用或者直接删除。这一步很多人会漏。source code scraping 是一个主动触发的功能而代码托管集成本身可能还会注册 webhook、定时同步 issue 关联关系。只要集成的授权 token 还在后台后台仍然可能因为其他任务访问外部平台。我们当时就是只关了功能开关忘了删集成导致抓包时还能看到外部 API 请求。停用集成之后最好也把 Sentry 里存储的访问令牌、Webhook URL 一并清掉。真正的安全加固不能只依赖“功能开关”而是要把执行权限本身收回来。3.4 第四步验证“真的关了”而不是“看起来关了”验证思路很直接构造一个探针事件让堆栈帧的文件名指向我们可控的内网地址然后看 Sentry 后台 worker 会不会访问那个地址。我在测试环境里通常准备一台内网监听服务比如在192.0.2.10:9001上跑一个nc -l 9001。然后在应用上报链路里利用 SDK 的before_send钩子把事件的堆栈信息改成探针地址。import sentry_sdk def before_send(event, hint): exception_values event.get(exception, {}).get(values, []) if exception_values: frames exception_values[0].get(stacktrace, {}).get(frames, []) if frames: frames[0][filename] http://192.0.2.10:9001/probe.txt return event sentry_sdk.init( dsnhttp://your-dsnsentry.local/2, before_sendbefore_send, )然后用正常代码触发一个异常上报到 Sentry。如果关闭前执行监听服务那边一定会出现连接关闭后再执行等几分钟都不会有连接。同时到 Sentry 后台打开这条事件正常情况下已经看不到“查看源码”的入口。这个验证方式一定要放在测试环境里做不要在线上随意构造带内网地址的事件避免干扰真实数据的统计。4. 踩坑实录配置“看似生效”却还在打外网的四个坑4.1 坑一改的是 web 容器干活的是 worker 容器自托管 Sentry 部署起来通常有 web、worker、cron 好几个角色。第一次改配置时我把环境变量加到了sentry-web这个服务的environment里web 容器起来后我检查env确实看到变量了但上报事件之后后台依然尝试去拉代码。原因很简单消费上报事件、执行源码抓取的是 worker 容器不是 web 容器。这个坑的排查成本其实不高只要先想明白“哪个进程会发起外部请求”再针对这个进程改配置就行。我的习惯是先用docker logs sentry-worker看看有没有异常再用抓包确认出口流量来自 worker 容器。确定目标后把配置统一放到全局.env或整个服务的公共环境变量里而不是散落在某个容器。4.2 坑二布尔值被当成字符串关了等于没关还有一次配完环境变量用docker compose config检查时变量值也是false但后台行为还是开启。仔细看了代码逻辑才发现启动脚本里对这类配置的判断不是严格的布尔比较而是走os.environ.get(SENTRY_SOURCE_FETCH_ENABLED)这类方式只要变量存在且非空字符串就被当成“开启”。如果走的是这种判断逻辑.env里写false反而会被当成真值因为字符串false不是空串。最稳妥的做法是在 Python 配置里写死布尔类型import os SENTRY_FEATURES[organizations:source-fetch] ( os.getenv(SENTRY_SOURCE_FETCH_ENABLED, true).lower() true )这样环境变量写false或0都能正确解析成 False。如果你用的是配置模板生成出来的.env中尽量不要给布尔值加引号加引号在多个解析逻辑里都容易出问题。4.3 坑三只关开关忘了收回集成授权功能开关关掉后出网流量不一定立刻停止。我遇到过关闭所有 source fetch 选项后tcpdump 还是能看到定时任务连接外部代码托管平台。查了 Sentry 后台才发现原来某个项目里还保留着代码托管平台的集成授权token 仍然有效后台的某个同步任务还在定期调用外部 API。这时候只是关 UI 开关不够必须把集成权限回收掉。操作路径是先进到组织设置里的集成管理把对应的外部代码平台安装停用如果确认彻底不用再删除相关配置。删除后建议重启一次 worker 和 cron让后台连接池和缓存任务全部清掉。如果你们使用的是多租户或项目隔离很严格的场景要特别注意集成授权可能存在组织级也可能存在项目级。收权时不能只关一个入口。我最后是通过脚本把所有项目都过了一遍确保没有残留授权。4.4 坑四升级脚本把配置“洗”掉了自托管 Sentry 有一段时间没升级后来跑了一次版本升级升级完成后检查安全配置发现之前关闭的功能又变回开启状态。原因是升级脚本会重新生成.env原来手工加进去的配置被默认配置覆盖了。从那以后我把关键安全配置都放进了基础设施即代码仓库。每次升级后跑一轮检查脚本核心断言就是grep -q ^SENTRY_SOURCE_FETCH_ENABLEDfalse$ .env如果配置丢了这条命令返回非零CI 流程直接失败。这样升级虽然可能把配置覆盖但发布流程会紧接着报错不会带着错误配置上线。如果你用 Kubernetes 部署更推荐把配置放到 ConfigMap 或启动参数里而不是依赖镜像里的默认文件。5. 关了 scraping 之后纵深防御还能做什么5.1 把 Sentry 后台出网流量限制成白名单如果你因为业务需要暂时不能彻底关闭 source code scraping至少要在网络层把出网范围限制住。Sentry 的 worker 容器不应该拥有访问任意内外网地址的能力。我们可以为 worker 单独配置网络策略只允许它访问必要的数据库、消息队列以及已知的代码仓库域名。在 Kubernetes 环境里可以这样限制apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: sentry-worker-egress spec: podSelector: matchLabels: role: sentry-worker policyTypes: - Egress egress: - to: - ipBlock: cidr: 10.0.0.0/8 - namespaceSelector: {} podSelector: matchLabels: app: sentry-db ports: - port: 5432 protocol: TCP - to: - ipBlock: cidr: 0.0.0.0/0 ports: - port: 443 protocol: TCP关键点是不要把云平台元数据地址比如常见的169.254.169.254放到白名单里。很多团队在出方向策略里放了一条默认路由到 0.0.0.0/0这等于没防。真要防住 SSRF就要把默认出网全部拒掉只开放明确需要的端口和域名。如果用的是 Docker Compose 或直接跑在云主机上也可以靠安全组和 iptables 做类似工作。总之原则只有一个worker 能访问的地址越少越好。5.2 限制上报入口减少被探测的机会SSRF 要触发前提是攻击者能把恶意事件送进 Sentry。因此在上报入口做限制也能明显降低风险。DSN 如果泄漏到公网任何人都可以构造事件所以我们要把上报链路控制到最小范围。比较有效的方法包括在反向代理层对上报接口做访问控制只允许内网 IP 或授权来源访问限制/*/store/路径的请求体大小对同一 DSN 的异常事件做频率限制。我们线上还加了更严格的规则如果堆栈帧里的filename或abs_path出现内网 IP、云元数据地址、file://协议等特征直接在上报侧丢弃或进入隔离队列不让它进入正常的抓取链路。在 Sentry SDK 侧before_send钩子也可以做简单的过滤。虽然它不能完全阻止攻击者直接构造请求但能有效减少误报和异常输入。5.3 用安全公告和版本升级兜底自托管 Sentry 的攻击面不是一成不变的官方在新版本里也会持续修复各种漏洞和请求过滤逻辑。之前某一版本对file://协议做了明确拦截而旧版本是可以直接尝试读取本地文件的。这类修复只会出现在后期版本里所以保持版本更新很重要。我的建议是关注项目发布说明中的安全相关条目发现涉及 SSRF、出入站请求、源码抓取的修复后安排一个低峰期窗口做升级。升级前在测试环境跑一轮同样的探针验证确认功能表现符合预期再切生产。另外安全加固尽量不要只依赖某一个开关。因为 Sentry 内部有很多功能都可能发起外部请求比如 webhook、release 文件下载、集成同步。source code scraping 只是其中最明显的一个。真正稳妥的做法是把它和网络出站白名单结合起来一起作为整体安全边界。这次加固给我最大的体会是遇到 Sentry 的安全告警先别急着改配置先想明白这个告警是从哪条路径打进来的谁会发起外部请求请求又会带上什么凭据。确认路径之后再动手关闭后一定要用探针验证而不能只看页面状态。source code scraping 对绝大多数内部团队来说价值确实远小于它带来的攻击面关掉之后错误堆栈照样能定位问题损失的最多只是“在页面上直接点开源码”的便利。最后再分享一个小技巧如果你后续还要保留其他出网功能记得把网络白名单一起做掉否则安全测试很可能换一个功能点又会把同类问题重新测出来。
阅读完成 · 觉得有帮助?
咨询建站