自建AI网关安全审计:虚拟key如何被串成一条提权链
你不会想到攻击者其实不需要突破网关的登录页——他只需要一把你随手发出去的虚拟 key。所谓“废钥匙”就是你发给测试同学、实习生或者某个自动化脚本用的虚拟 key预算 0.1 美元、只允许调用两个小模型、看起来人畜无害。结果某天账单异常你翻日志时才发现这把废钥匙竟然一路摸到了管理员抽屉。这种现象在自建 AI 网关上并不少见。我基于对 LiteLLM 这类代理网关的实战审计和社区反馈反复看到三类典型逻辑缺陷查询类端点权限校验缺失、日志接口字段回显过宽、密钥生成端点缺少权限上限校验。单独看每个都是“小毛病”但串起来就是一条从低权限普通 key 直达管理员的提权链。这篇文章就是来拆这条链的。1. 先搞清楚网关上的“门”是怎么装的1.1 自建 AI 网关到底解决了什么LiteLLM 这类项目之所以流行核心价值就四个字统一入口。团队内部不需要每个人都去注册云端大模型服务商的账号也不用担心 API key 散落在各处。网关统一接收请求做路由、配额、记账、缓存还能在后端多个模型服务之间做切换和降级。听起来很美好但代价是网关本身成了一个高价值目标。模型服务的原始 API key、团队身份信息、调用日志、预算数据全部集中在网关这一层。攻击者只要拿下了网关等于拿到了整条生产链路的所有权。这也是为什么网关的权限设计比网关功能本身更重要。1.2 三道门认证、授权、访问控制多数人对网关安全的理解停留在“有 key 就能进”但实际上LiteLLM 这类代理里同时存在三套独立的门虚拟密钥负责认证证明“你是一个被允许调用的人”。团队与角色负责授权决定“你能看见和操作哪些资源”。模型路由与配额负责访问控制决定“你能调用哪些模型、烧多少钱”。这三套机制互相独立看起来是好事但也埋下了隐患。因为独立配置意味着一个请求可以通过认证并不代表它有资格访问该资源一个 key 能调用某个模型也不代表它有资格管理其他 key。很多自建网关出问题问题恰恰出在“把认证当成了授权”。1.3 暴露面误区公网部署 默认配置 万事通 key我在不少团队见过同一个部署模型一台公网云服务器docker run 一条命令把网关跑起来端口默认映射到 4000主管理 key 用默认值或者一个简单口令然后给所有人发虚拟 key。这类部署最危险的倒不是管理 key 泄露而是虚拟 key 的边界没设好。LiteLLM 的虚拟 key 不是简单的一个字符串它身上挂着一堆属性所属 team、可访问模型列表、预算上限、角色信息、自定义 metadata。边界设得松任何一把 key 都可能变成撬棍。理解这三道门的配置方式是理解后续三个缺陷的前提。2. 缺陷一查询类端点只验“钥匙”不验“身份”2.1 现象普通 key 竟然能拉全系统的 key 列表我第一次发现这个问题是在对一个模拟项目的网关做权限审查的时候。当时手里拿到一把普通团队成员的虚拟 key预算不到一美元模型权限也只有两个小模型。本来只想看看自己的调用记录顺手试了一下请求/spend/logs——这个端点本意是给团队管理员看消费明细的。结果返回 200而且响应体里的数据不是我自己团队的那几条而是整个网关里所有团队、所有用户的调用日志。接着又试了/key/list也挺给面子直接列出了系统内一批虚拟 key 的信息包括 key 的哈希、关联 user_id、team_id、创建时间。一个测试用的低权限 key在没有管理员角色的情况下能够读取系统级的资源列表。这就是典型的 BOLA对象级越权问题翻译成大白话就是后端在判断“你有没有权限看这个资源”时只检查了你手里有没有一把有效钥匙而没有检查这把钥匙的等级。2.2 根因信息类端点缺少显式角色校验这类端点在代码里通常被设计成“管理员工具”但鉴权中间件在实现上常常写成请求头带了Authorization: Bearer 某个有效 key→ 放行请求头没带 key或者带了无效 key → 拒绝问题就在“某个有效 key”这几个字上。中间件没有区分这个 key 是管理员 key、团队管理员 key还是普通团队成员 key。只要 key 在数据库里存在、没过期、没被删除请求就通过了。为什么会这样我的判断是开发者默认“能拿到 key 的都是自己人”。在早期单机部署、团队全员互信的场景下这个假设成立。但一旦网关被用来隔离不同团队、不同项目甚至对外开放试用账号“能拿到 key”和“能信任”就不再等价了。这个默认假设没有随使用场景升级权限边界自然就漏了。2.3 哪些端点容易踩雷从反馈和审计经验看容易出问题的基本集中在三类查询类/spend/logs、/key/list、/user/info、/team/info这类只读接口统计类带聚合统计的 dashboard 接口比如按时间粒度拉全系统消费配置读取类某些配置信息接口允许带 key 就返回全局配置片段这些端点的共同特征是它们不直接改数据所以开发者容易低估泄露风险。但恰恰是只读接口攻击者最喜欢先摸一遍因为它们把内部结构全暴露了。2.4 验证方法两步就够了如果你想确认自己部署的网关有没有这个问题用一把最低权限的虚拟 key 做两个请求就能看出来# 请求1拉全系统消费日志 curl -H Authorization: Bearer 普通虚拟key \ http://你的网关地址/spend/logs?start_date2024-01-01 # 请求2拉 key 列表 curl -H Authorization: Bearer 普通虚拟key \ http://你的网关地址/key/list如果返回的数据里出现了不属于你当前 key 所属团队的内容或者干脆是完整的 key 元数据列表那基本可以确认缺陷一存在。注意这个验证动作只读不写不会有破坏性但正式生产环境操作前还是建议先在测试实例上做。3. 缺陷二日志接口把“钥匙坯”也一起交了出来3.1 现象消费日志里塞满了敏感字段如果说缺陷一是“打开了错误的门”缺陷二就是“门后面堆满了贵重物品”。/spend/logs这类接口为了满足审计和排障需求通常会把每个请求的细节都返回出来。问题在于返回字段远远超出了“看消费明细”需要的范围。一个典型的响应体里除了正常的模型名、token 数、花费金额、调用时间之外还包含这些字段hashed_api_key当前请求所用 key 的哈希值user_id发起调用的用户 IDteam_id所属团队 IDmetadata创建 key 时附加的元数据可能包含内部备注key_aliaskey 的别名管理员有时候会用它记内部标识单独看每个字段都像是排障必需的但拼在一起就成了一份“内部情报包”。这类接口在返回数据时没有做字段裁剪和脱敏等于把所有内部信息原样端了出来。3.2 根因日志为“全”服务不为“安全”服务日志接口的设计目标是让管理员能看清系统里发生了什么。所以开发者优先保证“信息全”包括关联到具体 key、具体用户、具体团队。这种设计本身没有问题问题在于它没有区分“谁能看这份日志”。缺陷一解决的是“谁能访问”缺陷二解决的是“访问之后能看到什么”。即使管理员把日志接口设成了 admin-only只要这个 admin-only 没有真正生效日志接口的全量字段就全部暴露在低权限请求面前。换句话说缺陷一打开了门缺陷二保证了门后面一定有好东西。3.3 信息如何变成武器哈希、别名、元数据很多人觉得“key 哈希泄露了也没啥反正不能反推”。这句话放在加盐哈希上成立但在某些实现里key 的哈希是确定性哈希也就是同一个 key 每次算出来都一样不加盐、不随机化。这意味着攻击者可以做一件事枚举。大模型 API key 的结构通常是固定前缀加一段随机字符长度和字符集都是公开的。攻击者可以先从已泄露的 key 列表里提取前几位判断格式然后自己生成大量候选 key用同样的哈希算法算一遍比对结果。如果管理员曾经用过一个弱随机性 key或者 key 是从某个凭证管理工具里导出的固定格式离线字典攻击完全可行。更直接的是metadata和key_alias两个字段。有些管理员在创建 key 时会顺手在 metadata 里塞内部信息比如“这个 key 对接某某自动化的凭证”甚至有人图方便直接把云端模型服务商的真实 API key 或内部 token 放在 metadata 里。一旦日志接口回显这些字段管理员以为藏在后台的机密实际上早已挂在查询接口的响应里等着被拉走。3.4 对比同类型网关谁做得好一点不是所有网关都这样。有的开源代理项目在日志接口上做了两层防护第一层是端点强制 admin-only第二层是即使管理员访问也对 key 哈希做“前缀截断”展示只显示前 6 位用于模糊匹配metadata 默认不返回。这个对比说明一个问题日志信息做脱敏并不复杂缺的只是意识。如果你的网关没有做这两件事那它大概率有缺陷二。4. 缺陷三密钥生成端点缺失权限上限校验4.1 现象低权限 key 能给自己造一把更高级的“钥匙”如果说前两个缺陷负责“读”第三个缺陷负责“写”。在我审计的某个配置组合里/key/generate这个端点对调用者角色的校验存在问题。正常情况下这个端点应该只允许管理员调用用来给新成员生成虚拟 key。但某些版本和配置组合下它校验的不是“调用者是不是 admin”而是“调用者有没有某个模型通配权限”。具体来说管理员有时会给自动化任务创建一把“万事通”型 key模型权限写成[*]方便脚本访问所有模型但角色并不是 admin。问题就来了如果/key/generate的鉴权逻辑写成“调用者的模型权限包含 * 就允许生成新 key”那这个本来只是模型权限宽、管理权限低的 key就能直接给自己生成一把拥有 admin 角色的新 key。这类问题的本质是模型权限和角色权限被混在了同一个平面里。一个 key 能访问所有模型不代表它有资格管理其他 key。但在某些实现里这两个维度被等价了。4.2 根因生成接口缺少“权限上限”约束一个安全的密钥生成接口至少要满足两个条件调用者必须有管理员角色垂直校验生成出的新 key 的权限范围不能超过调用者自身的权限范围上限校验缺陷三常见在两个条件缺一。缺少第一个条件的情况是接口被中间件放行任何持有有效 key 的请求都能进来。配合缺陷一和缺陷二攻击者已经摸清了系统内部 key 的分布这时再补一步精心的请求就能直接造出管理员 key。缺少第二个条件的情况更隐蔽即使调用者真的是某个团队的管理员它生成的子 key 权限也可以随意膨胀。比如一个团队管理员本来只能管理自己团队的 key但因为接口没有校验新 key 的 team 归属和权限字段它可以生成一个属于默认团队、模型权限为*、预算上限为无限的新 key。权限膨胀后配额和审计全成了摆设。4.3 配置里的隐形陷阱通配符 默认团队我在排查一个具体问题时发现管理员在配置里给某个自动化 bot key 设了use_models: [*]然后这个 key 因为“模型权限很宽”被部分接口当成了高信任凭证。这就是通配符的典型危害*本意是“访问所有模型”但在某些代码路径里会被隐式解读为“信任这个调用者”。另一个隐形陷阱是新建 key 的默认归属。如果/key/generate不要求调用者显式指定新 key 的 team服务端可能会把新 key 挂到默认团队下。默认团队通常拥有大量共享资源和高预算阈值结果就是你只是调了个接口却无意间把一把高权限的钥匙放到了不该放的位置。4.4 验证方法不要轻易写操作验证缺陷三需要谨慎因为/key/generate是写操作会在系统里真实创建一个 key。最稳妥的做法是在测试实例上操作用一个模型权限为*但不是管理员角色的 key调用生成接口看返回的新 key 是否拥有超出预期的权限。curl -X POST http://你的网关地址/key/generate \ -H Authorization: Bearer 模型权限为*但非admin的key \ -H Content-Type: application/json \ -d {role: admin, models: [*], max_budget: 1000}如果返回 200并且新 key 真的具备 admin 角色那说明这台网关的密钥生成接口已经可以被人为提权了。请记得在验证后立刻删除或禁用这个新生成的 key。5. 一条链串起来从普通 key 到管理员的全过程5.1 攻击者的起点一把普通 key很多人觉得“我的 key 预算就一毛钱能有什么风险”。但攻击者最擅长的恰恰是把手里的低级权限一点点滚成高级权限。起点只需要一个普通团队成员的虚拟 key来源可以是钓鱼邮件骗来的、旧同事离职没回收的、测试脚本误传到公网仓库的。这把 key 不需要有多高权限只要还有效链条就能开始走。5.2 Step 1横向读取全系统 key 元数据攻击者先拿这把普通 key 打探环境。通过缺陷一请求/spend/logs和/key/list不费任何力气就拿到了全系统 key 的哈希列表、user_id、team_id、创建时间。这一步的本质是“横向越权”从只能看自己的数据扩大到看所有人的数据。5.3 Step 2从日志和元数据中提取高价值情报拿到列表后攻击者开始筛选高价值目标管理员创建的 key、关联到默认团队或高预算团队的 key、创建时间最早的那批 key。通过缺陷二从日志响应里翻出这些 key 的哈希、别名和 metadata。如果运气好metadata 里直接泄露了内部管理凭据如果没有攻击者可以拿着确定性哈希去离线枚举还原明文。5.4 Step 3生成管理级 key完成垂直提权拿到管理员级别 key 的信息后攻击者调用/key/generate利用缺陷三的权限校验缺失生成一个新的 admin key。这个新 key 完全合法拥有全量模型访问权限、无限制预算、管理端点全部开放。此时攻击者已经获得了和网关管理员同等的控制权。5.5 最终影响一张影响地图权限等级攻击者能做什么严重程度普通 key调用受限模型、消耗自己配额低越权读取缺陷一二读取全系统 key 元数据、日志、内部配置中高密钥生成缺陷三生成任意权限 key访问所有模型、清空预算、读取后端原始配置严重完全接管修改路由规则、中断服务、窃取所有调用记录和模型凭证致命5.6 为什么三者缺一不可单独看缺陷一只是“多看了点日志”缺陷二只是“信息泄露”缺陷三也只是“接口校验少了点”。但串起来之后它们形成了完整的攻击链缺陷一解决访问入口问题缺陷二解决情报收集问题缺陷三解决权限扩张问题。三个缺一攻击者要么进不来、要么不知道该往哪走、要么拿到了情报也造不出高权限 key。所以修复时也不能只堵一处而是要三处一起封。6. 自查手册怎么判断自家网关是否被敲过门6.1 配置自查清单先做静态检查确认自己的部署是否存在结构性问题网关是否直接绑定了公网地址管理端口和业务端口是否分离主管理 key 是否使用默认值或简单口令是否给非管理员 key 配置了use_models: [*]日志接口是否区分 admin 与普通角色是否对/key/generate做了角色校验和权限上限校验是否有审计日志记录 key 的创建和删除事件以上任何一项的回答是“否”都说明当前网关的权限边界存在风险敞口只是可能还没被利用而已。6.2 日志里的异常信号如果你怀疑已经被攻击者“敲过门”重点看这几类行为低权限 key 高频访问/spend/logs或/key/list尤其是非工作时间请求序列呈现“先扫查询接口再调生成接口”的规律日志中出现大量新生成的 key且权限远超创建者的正常用途某个 key 访问了它从未访问过的管理端点然后马上消失消费日志中出现没有对应团队归属的调用记录这些信号单独看可能都能解释过去但多个同时出现就要立刻按安全事故处理。6.3 最小复现脚本我建议每个自建网关的维护者都准备一个最小复现脚本定期用最低权限 key 自查一遍#!/bin/bash # 自检脚本用普通 key 测试管理端点是否越权 GATEWAYhttp://你的网关地址 LOW_KEYsk-你的低权限key echo [1] 尝试读取全系统 key 列表 curl -s -H Authorization: Bearer $LOW_KEY $GATEWAY/key/list | head -c 500 echo echo [2] 尝试读取全系统消费日志 curl -s -H Authorization: Bearer $LOW_KEY $GATEWAY/spend/logs | head -c 500 echo echo [3] 检查 key/info 是否可查询他人信息 curl -s -H Authorization: Bearer $LOW_KEY $GATEWAY/key/info | head -c 500 echo 这个脚本只做读操作不会破坏数据建议放进定时任务每周跑一次输出变化做对比。如果你发现某个原本应该 403 的接口突然返回 200说明权限配置被动过手脚需要立刻排查。7. 把门焊死修复与加固清单7.1 立即加固升级、关闭、裁剪最直接的动作是升级到修复版本。如果你用 LiteLLM 这类项目留意官方发布的鉴权相关更新尤其是在角色校验、日志字段裁剪、密钥生成权限上限这些方面的改动。暂时无法升级的情况下先在反向代理层做拦截把/key/generate、/key/list、/spend/logs这几个高危端点限制为仅内网 IP 或管理员 IP 可访问用一层“物理隔离”暂时代替逻辑校验。同时裁剪日志接口的返回字段。宁可让排障麻烦一点也不要让敏感字段出现在响应体里。key 哈希只保留前几位metadata 一律不返回key_alias 用脱敏后显示。7.2 权限模型整改角色与模型权限分离这是从根上解决问题的一步。明确区分“模型权限”和“管理权限”模型权限决定 key 能调用哪些模型用具体列表而不是*管理权限决定 key 能否管理其他 key、团队、预算不要让“能访问所有模型”和“信任这个调用者”之间产生任何隐式等价。如果确实需要某个 bot key 访问所有模型那就明确给它一个角色但这个角色不应该是 admin也不能作为调用/key/generate的凭证。/key/generate这个端点必须同时满足两个条件调用者具备管理员角色且生成的新 key 权限范围不超过调用者权限上限。建议加一层代码断言在返回前自动校验新 key 的权限字段一旦超过调用者自身权限就拒绝生成。7.3 部署层面加一层真正的门网关本身做好权限校验之后部署层面也不要裸奔。几个关键动作网关只监听内网地址公网访问一律走反向代理反向代理层增加一层身份认证比如企业 SSO 或基础认证认证通过后再转发到网关管理端点和管理 key 的使用独立成单独的入口不与业务入口混用容器网络层面做隔离让网关无法访问不必要的内部系统这些手段不解决网关自身的逻辑漏洞但能极大抬高攻击者的门槛。攻击者哪怕拿到了虚拟 key也得先过反向代理这一关。7.4 监控与告警让“敲门声”变得响亮部署层面的加固做完还要解决“看不见”的问题。建议对以下事件做实时告警任何非 admin 角色请求管理端点直接触发告警/key/generate被调用时记录调用者的 key、角色、生成 key 的权限字段某个虚拟 key 在短时间内的访问模型范围发生跳变预算消耗速率超过历史基线告警的意义不是阻止攻击而是缩短发现时间。提权链再复杂总会留下痕迹只要能在几分钟内发现损失就可控。7.5 应急响应如果已经被提权了怎么办假设排查后确认已经被提权按这个顺序处理立刻轮换主管理 key 和所有云端模型服务商侧的真实 API key删除或禁用所有可疑的新生成 key重置网关配置文件重点检查 route 规则和预算设置有没有被篡改导出并从备份中比对关键配置变更时间线全面审计 spend logs列出被调用的模型和 token 总量评估实际损失整个过程不要单兵作战如果团队里没有专职安全人员建议找有审计经验的人帮忙复查一遍配置和日志。8. 写在最后自建网关的权限设计心得我踩过几次坑之后最深的体会是自建网关的安全边界不是靠主管理 key 一把锁撑起来的而是靠每一把虚拟 key 的权限边界撑起来的。这个项目也好其他同类网关也好真正决定安全性的不是项目知名度而是你有没有把“最小权限”四个字落实到每一把 key、每一个端点、每一次日志返回上。另外分享一个小技巧我习惯在每台网关的维护脚本里加一条自检命令用团队里权限最低的那把 key 定期去请求管理端点。如果哪天它返回了不该返回的数据说明权限边界已经被某次配置修改破坏。这个习惯帮我提前发现过一次因误操作导致的越权比任何告警系统都直接。建议你也给自家网关配上这个“定期敲门”的测试脚本。