
项目是我自己在维护的内网扫描服务。xray用得久了有个绕不开的问题默认监听端口谁都能连只要知道地址随便一个人都能把扫描任务调起来甚至能看到别人提交的检测目标。公司内部还好一旦跨部门协作或者需要远程接入这种“裸奔”状态就非常尴尬。我花了一段时间把xray的访问控制做了改造让它同时支持匿名、授权、IP白名单三种访问方式。这篇文章就把改造思路、实现方式、部署用例和踩过的坑完整记录下来给同样需要给xray或其他Web服务做访问控制的朋友一个可以直接参考的样本。1. 改造背景与方案设计思路1.1 为什么要动xray的访问控制先说场景。我这边xray不是单机自用是作为一个内网漏洞检测服务开放给团队成员使用。团队里有人做渗透测试有人做等保自查也有人只是临时想确认某个接口是否存在已知漏洞。暴露出来的问题有三个第一没有任何鉴权措施。xray默认启动后监听在本机或指定地址的端口任何能访问到这个端口的人都可以直接使用。在内网里只要有人扫描到端口或者看到文档里的地址就能操作扫描器。这对内部安全工具来说算是比较严重的短板因为工具本身能发起主动探测一旦被滥用后果不轻。第二使用行为无法追踪。裸奔状态下日志里只有请求记录没有用户身份。没法知道某个扫描任务是哪个部门、哪个同事发起的出了问题要回溯就只能抓瞎。第三远程接入场景无法收敛。随着协作范围扩大会有人从家里、从办公网外部连回内网使用xray这时如果还是完全开放访问风险就会成倍放大。所以改造的核心目标很明确在不改变xray原有扫描能力的前提下在HTTP入口层做一套访问控制让管理员可以按场景选择匿名放行、授权校验、IP白名单限定三种方式或者自由组合。1.2 三种访问方式各自解决什么问题结合运营实际我把三种方式的使用场景拆开来看。匿名访问就是不加任何校验直接放行。适合的场景是xray部署在本机回环地址127.0.0.1只给本机工具调用或者部署在完全隔离的专用网段且明确不需要身份区分。这种方式的优点是零接入成本启动即用缺点是几乎没有安全边界所以我对它的定位是“默认选项”而不是“推荐选项”。授权访问指的是请求方必须携带有效的凭证才能通过。我采用的是Bearer Token方式也就是请求头里带一个预先分配的API Key服务端校验通过后才放行。适合的场景是多人共用一个扫描服务需要区分身份、追踪行为或者服务暴露在不完全可信的网络环境。授权访问的核心价值在于“可控”管理员可以随时吊销某个key也能在日志里识别出是谁在调用。IP白名单访问指的是源IP地址必须在允许列表内才放行。适合的场景是服务固定在内网使用团队成员的办公IP相对稳定或者xray放在docker网关后只希望特定来源的流量进来。IP白名单的好处是无需客户端做任何改造网络层直接过滤性能开销极低缺点是不能拒绝来自白名单内设备的滥用因为IP本身不能代表身份。设计这三种方式的时候我有一个原则能组合就不单选能按路径划分就不全局一把梭。因为实际使用中往往是混合需求比如管理接口需要授权而提交扫描任务的接口只需要白名单内放行。单一策略看着简单真用起来会遇到很多别扭的场景。2. 核心改造实现访问控制模块拆解2.1 先设计一个够用的配置结构改造首先从配置入口开始。xray的配置是YAML格式我新增了一个access_control配置块结构设计为access_control: # 全局默认策略allow / deny default_policy: deny # 匿名访问值为 true 时完全放行 anonymous: false # 授权访问 auth: enabled: true # 可签发多个 key便于按人分配 tokens: - xray_ops_7f8a2b9c - sec_team_e1d4f6a8 # IP 白名单 ip_whitelist: enabled: true # 支持 CIDR ranges: - 10.10.0.0/16 - 192.168.1.100/32 - 172.16.3.0/24 # 按路径覆盖规则可选 rules: - path: /web policy: auth - path: /api/submit policy: ip_whitelist - path: /api/status policy: anonymous有几个设计点我想特别说明。default_policy我建议默认设成deny。这样即使某个规则没写好也不会出现“忘了配结果全放开了”的尴尬。安全工具的第一原则是默认拒绝白名单心态而不是默认允许。rules这个字段是后来补充的但实际用下来非常必要。因为xray的不同路径安全等级完全不同比如/web的管理界面绝对不能用匿名方式而/api/scan/status这种状态查询接口在内部访问时放行反而能减少很多测试流程的阻力。用路径级规则做覆盖相当于把“全局强制策略”和“局部灵活策略”结合起来。还有一点要提醒Token的存放和管理。token是明文写在配置里的想完全防住能读到配置的人是不可能的所以我的实现里不影响文件权限约束。在部署文档里我也特意标注配置文件权限需要收敛到启动用户避免普通用户直接读取。2.2 请求链路中的拦截顺序访问控制模块本质上是插入到xray HTTP服务前的中间件。处理逻辑我简化成下面这段Go伪代码实际实现也基本是这个流程func AccessControlMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 1. 如果配置了按路径规则先匹配路径级策略 for _, rule : range cfg.Rules { if strings.HasPrefix(r.URL.Path, rule.Path) { if !checkPolicy(rule.Policy, r) { rejectRequest(w, r) return } next.ServeHTTP(w, r) return } } // 2. 没有路径规则时走全局策略 if cfg.Anonymous { next.ServeHTTP(w, r) return } if cfg.Auth.Enabled { if !checkAuth(r) { rejectRequest(w, r) return } } if cfg.IPWhitelist.Enabled { if !checkIP(r) { rejectRequest(w, r) return } } // 3. 全局策略为 deny 时上面所有检查都没放行就拒绝 next.ServeHTTP(w, r) }) }这段代码里有两个关键细节。判断顺序上我是路径规则优先于全局策略。因为路径规则往往表达更具体的需求比如“即使全局开了IP白名单健康检查接口也允许匿名访问”。路径规则先把请求分流掉剩下的请求再走统一策略。checkPolicy内部会根据策略类型复用同一个校验函数。这里没有把匿名和授权做成互斥关系而是让它们可以叠加比如一个路径要求“白名单内IP且携带有效token”写成策略组合就行。这样设计的灵活性在真实运营中很重要后面配置示例里我会展开讲。拒绝响应我统一返回403 Forbidden响应体里只含{error:access denied}。不返回401的原因是401会触发客户端的认证交互流程对脚本调用方来说反而会产生额外请求403更直接语义也准确当前请求被访问策略拒绝了。2.3 授权校验与IP校验的具体实现授权校验这里我采用Bearer Token读取Authorization头格式为Authorization: Bearer token。读取后与配置里预置的token做常数时间比较subtle.ConstantTimeCompare避免时序侧信道攻击。另外顺手兼容了X-API-Key头因为自动化脚本里很多人习惯用这个。两种方式都检查任何一个通过都算合法。IP白名单校验是重头戏。核心实现要点如下func ipInRanges(ip net.IP, ranges []string) bool { for _, r : range ranges { // 把字符串解析为 *net.IPNet _, ipNet, err : net.ParseCIDR(r) if err ! nil { continue } if ipNet.Contains(ip) { return true } } return false }这里必须使用net.ParseCIDR而不是简单的字符串前缀匹配因为CIDR掩码计算是网络层的正确语义。比如10.10.0.0/16表示的是10.10.0.0到10.10.255.255之间的所有地址用字符串前缀匹配很难精确表达这个范围。IP来源取的是TCP连接的对端地址也就是socket addr而不是HTTP头里的X-Forwarded-For或RemoteAddr直接解析。这一步特别重要原因我在第4章踩坑部分会详细说简单提一句HTTP头是客户端可控的直接用等于白名单形同虚设。IPv6也需要一并支持。内网环境现在IPv6已经很普遍了很多自动化任务跑在K8s集群里Pod访问出口IPv6地址。如果白名单只写了IPv4那这些请求会全部被拒排查起来很困惑。所以ipInRanges里同时兼容两类IP配置示例我也放了一个IPv6的CIDR。3. 实操部署三种方式的配置样例与组合用法3.1 单人本机匿名加本地回环限量版先给最简单的场景写一份配置。我个人机器上跑xray做自用扫描时访问控制的考虑就一条其他程序不能防但端口不能暴露到局域网。配置如下access_control: default_policy: deny anonymous: true ip_whitelist: enabled: true ranges: - 127.0.0.1/32 - ::1/128这样的配置效果是只有本机回环地址的请求能进来且因为anonymous: true不需要任何token。本质上是用IP白名单把服务的网络暴露范围钉死在本地再用匿名方式省去认证的麻烦。如果只是单纯xray webscan --listen 127.0.0.1:8080那其实连改造都不用做。但我的场景里经常需要从容器里或者本机其他进程发起请求端口是绑定在0.0.0.0上的所以必须靠白名单来兜底。这也是我建议不管什么场景都至少开启IP白名单的原因即使不需要身份认证也要把可访问源收窄到可信范围。3.2 团队内网IP白名单加授权双保险团队成员使用xray的典型配置我推荐“IP白名单授权”同时启用。白名单用来收敛来源token用来区分身份两者互为补充。access_control: default_policy: deny anonymous: false auth: enabled: true tokens: - admin_token_2025 - pentest_li_0921 - pentest_wang_1218 ip_whitelist: enabled: true ranges: - 10.10.0.0/16 - 172.16.0.0/20这里我做了两个关键决策。一是anonymous关掉意味着任何请求都必须通过授权校验才可能被放行。这样无论IP是否在白名单内没有合法token的请求直接拒绝。有些人会觉得矛盾既然IP白名单已经限制了来源为什么还要token答案是白名单只能说明“请求来自可信网络”不能说明“请求者是本人”。内网里一台办公电脑被恶作剧者扫到端口或者员工无意触发了扫描都会造成困扰。token相当于第二道身份关卡。二是token按人分配。我给每个人一个唯一的token不是为了防谁而是为了在日志里能精确追溯到人。改造时顺手把token的别名记录在配置注释里比如谁负责渗透测试、谁是管理员这样排查问题或者做安全审计时会顺畅很多。还有一个实践技巧给token设置合理的命名规律不要用随机字符串一把梭。比如pentest_li_0921这种格式任何人都能明白是哪个项目哪个人的凭证。虽然token本身应该保密但可读的命名会让运维和协作都轻松不少。3.3 按路径区分访问策略的进阶用法团队协作之后需求很快就变细了。有人反馈扫描状态查询如果是写自动化脚本每次都要带token很麻烦还有人觉得扫描结果下载不应该人人都能拿到。这其实就是不同路径对应不同安全等级的典型场景。路径级规则配置如下access_control: default_policy: deny anonymous: false auth: enabled: true tokens: - admin_token_2025 ip_whitelist: enabled: true ranges: - 10.10.0.0/16 rules: - path: /web policy: auth - path: /api/scan policy: ip_whitelist - path: /api/status policy: anonymous - path: /api/report/download policy: auth解读一下/web管理后台只允许授权用户访问token校验。/api/scan提交扫描任务只要来源IP在内网白名单内就放行方便脚本直接调用。/api/status查询任务状态完全匿名放行因为这类请求不泄露敏感数据只返回任务进度。/api/report/download下载报告强制授权防止敏感扫描报告被未认证人员拉走。这个配置在团队里落地后一个很直观的变化是自动化脚本不再需要处理token直接内网调用/api/status和/api/scan就行而管理操作和报告下载仍然保留强校验。按路径隔离不同敏感级别的能力比全局策略好用太多这也是我认为改造中最值得复制的一部分。顺带说下路径匹配的细节我实现的是前缀匹配不是精确匹配。因为很多真实路径带动态参数比如/api/scan/result/1024。用前缀匹配只要配置了/api/scan其下所有子路径都会继承同样的策略。如果确实需要对某个子路径单独设置把更长的路径规则放前面前面匹配到就终止这样能形成从具体到宽泛的优先级链。4. 踩坑记录与问题排查实录4.1 最常见的坑拿到的是假IP这个坑我必须放在第一位说。改造第一版上线后我在白名单里放行了10.10.0.0/16结果发现来自办公网外部的请求也能访问日志里记录的来源IP全是127.0.0.1。当时差点怀疑白名单判断逻辑写错了。排查下来发现两个层面的问题。第一xray所在主机上很可能有反向代理或端口转发比如nginx转发、docker端口映射。这时传给xray的连接对端地址是代理服务器的地址比如127.0.0.1或docker网桥地址而不是真正的客户端IP。如果白名单只看这几层等于对所有来源全放。解决方法是如果前面有可信代理需要额外配置一个trusted_proxies列表在代理链中提取第一个非代理的客户端IP。注意这个列表必须由运维显式指定绝不能默认信任X-Forwarded-For否则客户端可以直接伪造这个头来绕过白名单。第二日志分析时也要用同一个逻辑。访问日志里的IP列记录的是处理后的“真实来源IP”而不是原始连接IP。这样排查问题才不会被误导。我给配置加了一个字段access_control: # 标记可信代理 trusted_proxies: - 127.0.0.1/32 - 172.18.0.1/32只有连接来源IP在trusted_proxies里才允许从X-Forwarded-For中提取真实IP。否则一律以socket对端地址为准。4.2 Token泄露的防不胜防本来以为token政策落地后访问控制就稳了。但实际考察了一圈使用姿势发现Token泄露路径比我预想的多得多有人把token直接明文写在命令行参数里通过ps就能看到。有人在浏览器收藏夹里保存了带token的URL虽然我用的是Header而不是URL参数但同学安全意识不到位会把token拼成/api/status?keyxxx去试结果被日志记录下来。还有人为了方便把token写在CI脚本的明文配置里虽然仓库是私有仓库但一旦可见范围扩大到外包团队token就等于半公开。针对这些情况我的处理策略有三条在文档里明确要求token通过环境变量传递不要写死在命令和脚本里。在xray的访问日志中对Authorization和X-API-Key字段做脱敏处理只记录token的前四位和后四位比如sk43****a8f1。这样即使日志被翻到也不会直接泄露完整token。在配置里增加token轮换机制。虽然不能做到自动轮换但文档明确写明每个token有效期不超过90天到期手动更新配置并重启服务。这个机制能减少长期token被滥用造成的影响面。4.3 网络边界上容易被忽略的IPv6IP白名单改造完成后有个同事反馈他的K8s集群任务请求全部被拒。看了日志来源IP是fe80::...或者fd00::...开头的一长串地址。原因很简单白名单里只配了IPv4的CIDR没配IPv6。K8s集群内Pod访问外部服务时如果出口IP是IPv6那请求就会在IP校验时直接落空。解决方式是把内网IPv6地址段也加进白名单。比如ip_whitelist: enabled: true ranges: - 10.10.0.0/16 - fd00::/8fd00::/8是本地站点IPv6地址的保留段和IPv4私网段类似用于内网通信。实践中先把fd00::/8配进去因为K8s和容器网络的IPv6地址基本都在这个范围。如果还有更具体的管理网段再细分。经验是做IP白名单时IPv4和IPv6要一起规划不能只考虑办公网那一段。尤其是容器化和云原生环境普及之后出口IP的地址族往往和传统办公网不一样。4.4 性能损耗与兼容性有朋友会担心加了访问控制中间件会拖慢扫描器的吞吐。实测下来这个担心可以放下。访问控制的性能开销主要在两部分IP白名单的CIDR匹配以及token的字符串比较。CIDR匹配用net.IPNet.Contains实现底层是按位与运算几万条白名单规则的开销也就是微秒级别。token比较采用了常数时间比较也是微秒级。整体对xray的QoS影响可以忽略不计。真正需要注意性能的是日志记录。如果把每一次请求的完整头信息都打到日志里当扫描吞吐高时日志量会非常大磁盘I/O会成为瓶颈。我实现时对访问日志做了专门处理只记录路径、来源IP、策略命中结果、token脱敏值并且对状态查询这类高频接口做了采样记录默认每10秒聚合一条。这样既保留审计信息又不会拖垮磁盘。兼容性方面有一个细节要特别提醒xray自身发出的探测请求走的是内部扫描逻辑不走HTTP服务入口。所以访问控制中间件不会拦截扫描引擎发出的请求不用担心改造后扫描结果无法回传。另外我用独立模块方式实现长期维护下来发现这种方式非常友好。升级xray内核组件时只需要重新编译不需要改动扫描链路本身。如果哪天官方加入了原生的访问控制功能这个模块也可以直接下线不影响其他部分。4.5 常见问题速查表现象可能原因排查思路请求被403拒绝IP不在白名单内检查来源IP是否匹配CIDR尤其注意IPv6请求被403拒绝token无效确认Authorization头格式注意Bearer前缀白名单内设备访问被拒前面有代理拿到了代理IP配置trusted_proxies并启用真实IP提取匿名模式仍然被拒default_policy为deny且未显式开启anonymous确认anonymous是否设置为true日志里的IP全是127.0.0.1有反向代理或docker映射检查网络链路按代理链提取真实IPtoken泄露后无法快速止血没有token轮换机制尽快配置可轮换token并更新文档路径规则不生效前缀匹配被更早规则截断调整rules顺序把具体路径放在前面结语改造之外的一些体会这次改造我自己总结出一个更通用的方法论给工具加访问控制的时候不要一上来就想“用哪种最安全”而是先回答三个问题——谁能连、怎么证明身份、哪些操作需要更严格限制。三个问题想清楚了选型和实现都会快很多。xray这次改造的匿名、授权、IP白名单本质上就是这三要素的三种具体形态。后面如果你们也要给内部系统做类似能力我建议直接复用这套思路先定默认拒绝策略再逐层开端口比先全开放再封堵要省心得多。