自建网关安全加固:API 网关别裸奔在公网上
先把这件事的性质说清楚:你自建的那台网关,后台里躺着你所有上游供应商的原生 key。 不是一把,是全部。你接了几家,它就存了几家的凭证。
这意味着网关的安全等级不该按「一个内部小工具」来定,而应该按密码库来定。可现实里我见过太多次的部署姿势是:一台云主机,跑起来,端口开到公网,管理后台直接能访问,然后把地址发到群里让同事自己去建账号。这台机器从那一刻起就是裸奔的。
裸奔的代价还有一个容易被低估的地方:这是一种会持续烧钱的失陷。 数据库被拖走,你至少能从日志里看到一次异常访问;但网关被拿下之后,攻击者不需要做任何”异常”的事——他就用你的网关、你的额度、你的上游 key,正常地调模型。请求成功、响应正常、监控面板一片绿。你大概率要等到月底看上游账单,才发现这个月多出来一大笔用量。中间那几周,钱一直在流。
所以这篇按优先级排,从最外层的网络边界往里讲,每一层都给能直接照做的最小方案。
一、网络边界:这是第一优先级,其他都排在它后面
很多加固文章上来先讲密码强度、讲权限模型。顺序反了。如果管理后台能从公网直接打开,后面所有的措施都只是在给攻击者增加几分钟的工作量。
管理界面和 API 端点要分开对待
自建网关实际上提供两类完全不同的东西:
| 端点类型 | 谁在用 | 调用来源 | 该不该暴露公网 |
|---|---|---|---|
| API 调用端点 | 你的业务应用 | 应用服务器、边缘函数、有时是客户端 | 视架构而定,通常必须 |
| 管理后台 / 管理接口 | 你和少数几个同事 | 办公网、运维跳板机 | 默认不该 |
这两类的访问模式差别极大:API 端点是高频、机器发起、来源可能分散;管理后台是低频、人工发起、来源极其收敛——通常就是几个固定的人,从几个固定的地方访问。
低频且来源收敛的东西,就应该用最严格的方式关起来。 这是整篇文章里最省力、收益最高的一条。
以 New API 为例,它的默认端口是 3000(官方 README 给出的部署命令里 -p 3000:3000 就是这个端口)。很多人的做法是安全组直接放行 3000 到 0.0.0.0/0,然后管理后台和 API 一起暴露出去了。正确的姿势是把这两条路径拆开走。
可执行的最小方案
按你的实际条件,从上到下选第一个你能做到的:
方案 A(最优):管理后台完全不出内网。 网关容器只在内网地址上监听管理端口,需要管理时通过 VPN 或跳板机进内网访问。云上的话就是安全组只放行内网网段,公网入口一律不给管理路径。这个方案的好处是攻击面直接归零——不是”更难攻破”,是根本打不到。
方案 B:必须走公网,就在前面加一层访问控制。 用反向代理做前置,只把 API 路径放行到公网,管理路径要么按来源 IP 白名单放行,要么在反代层再加一道独立于网关自身的认证。关键点是:这道认证要独立于网关的账号体系。 如果网关自己的登录逻辑出了问题,你还有一层能挡住。
方案 C(保底):至少改掉默认端口 + 收紧来源。 这是最弱的一档,只能挡住无差别的端口扫描,挡不住任何有针对性的攻击。如果你现在是裸奔状态,这至少是今天下午就能做完的止血动作,但别把它当成终点。
反向代理 + HTTPS,不要让凭证走明文
不管选哪个方案,网关前面都应该有一层反向代理,并且全链路 HTTPS。原因很直白:
- 你的下游应用调网关时,Bearer token 是明文放在 HTTP 头里的。走明文就是在链路上广播你的凭证。
- 你登录管理后台时,密码和会话 cookie 同理。
- 加了 HTTPS 之后,日志、限速、来源 IP 记录这些能力也顺带在反代层拿到了,后面讲审计时会用上。
顺手提一句容器编排层面的事:官方推荐的部署方式是 docker-compose up -d,compose 文件里的端口映射写法决定了容器端口暴露到宿主机的哪个地址。把映射写成只绑内网地址,比在防火墙层补救更可靠——因为防火墙规则会被人改,而 compose 文件在版本管理里,改动看得见。
二、凭证管理:三个”不进”
上游 key 该怎么传给网关?答案是环境变量或密钥管理服务注入,并且守住三个”不进”:
不进镜像。 把 key 写进 Dockerfile 或者打包进镜像层,等于把凭证发给了所有能拉到这个镜像的人。镜像层是可以被逐层解开看的,删掉那一层也没用,历史层还在。
不进 git。 这个大家都知道,但真正出事的往往是 docker-compose.yml 这类”看起来是配置不是密钥”的文件——里面的 SQL_DSN 带着数据库密码,-e SQL_DSN="root:password@tcp(host:3306)/db" 这种官方示例格式一眼就能看出账号密码在哪。把这类文件写进 .gitignore,仓库里只留一份不含真值的模板。
不进 shell history。 用 docker run -e KEY=真值 这种方式手敲命令启动,那把 key 就留在了 .bash_history 里。谁能读到这台机器的用户目录,谁就能读到它。用 env 文件配合权限位(只有运行用户可读)比命令行传参安全得多。
网关自己的两个 secret 也是机密
New API 有两个环境变量特别值得单独拿出来说,因为它们看名字像”配置”,实际上是”机密”:
SESSION_SECRET—— 多节点部署时必须设置,且所有节点必须一致。它决定了会话凭据的有效性。CRYPTO_SECRET—— 缓存键的 HMAC secret,默认取SESSION_SECRET。
这两条放在一起看,结论很清楚:SESSION_SECRET 一旦泄漏,你丢的不只是会话,因为 CRYPTO_SECRET 默认就是它。 一个值同时兜着两件事。
所以实践上有两点:
- 把
SESSION_SECRET按最高等级的机密来管,跟上游 API key 同一个待遇——同一套注入方式、同一套轮换流程。 - 如果你的场景允许,给
CRYPTO_SECRET单独设一个值,不要让它继续走默认继承。这样两者解耦,一个泄漏不至于全线失守。
另外,多节点部署时”所有节点必须一致”这个约束,在实践中会诱导出一个坏习惯:为了图省事,把这个值硬编码在部署脚本里,或者干脆用一个好记的短字符串。这两个都是坑。正确做法是用足够长的随机值,通过配置中心或密钥管理服务分发到各节点,节点侧只从环境变量读。
数据库账号:单独建,最小权限
网关连数据库用的 SQL_DSN,官方示例里写的是 root:password@tcp(host:3306)/db。示例就是示例,别照着用 root 跑业务。
理由不是”root 更容易被猜到”,而是爆炸半径:网关被拿下之后,攻击者拿到的是 DSN 字符串里的那个账号。如果那是 root,他拿到的是整个数据库实例——包括跑在同一实例上的其他业务库。如果那是一个只对网关自己的库有读写权、没有建库删库权、没有其他库任何权限的专用账号,损失就被框在了这一个库里。
同样的逻辑适用于 REDIS_CONN_STRING:Redis 如果和其他业务共用实例,网关失陷就意味着其他业务的缓存也失陷。能独立实例就独立,不能就至少做逻辑隔离并给独立凭证。
至于网关运行的容器本身,也别用宿主 root 用户跑。这属于容器安全的通用实践,具体做法以你所用编排工具的官方文档为准。
三、最重要的一条架构建议:下发给调用方的必须是虚拟 key
前面两节讲的是”别让人打进来”。这一节讲的是打进来之后损失多大,以及日常使用中最常见的泄漏源怎么堵。
先说一个现象。很多团队自建了网关,然后……把上游的原生 key 直接发给了业务组。理由通常是”网关还没配好用户体系,先这样临时用”。这个”临时”会活很久。
下发给调用方的,一定要是网关签发的虚拟 key,绝不能是上游原生 key。 这是整篇文章里唯一一条属于架构层面的建议,也是收益最大的一条。收益有三个,逐个说透。
收益一:泄漏时只需要停一把
原生 key 发出去之后,它就在若干个你控制不了的地方了——同事的本地环境、CI 的变量、某个前端项目的构建产物、某个人的笔记本。任何一个地方泄漏,你都必须去上游控制台轮换那把 key,然后通知所有用它的人一起改。 这个过程要多久,取决于你能不能找全所有使用点。通常找不全。
虚拟 key 的模型完全不同:每个调用方一把独立的 key,泄漏了就停那一把,其他调用方毫无感知。上游的原生 key 从头到尾没动过,也不需要动。
关于”停”这个动作,LiteLLM Proxy 的虚拟 key 体系提供了 /key/block 和 /key/unblock 这一对接口。出事的时候应该先 /key/block 停用,而不是直接删除。 原因是取证:
- 删掉之后,这把 key 的关联信息就没了,你很难再回溯它到底是谁的、之前调了什么。
block之后 key 立即失效(止血目的达到了),但记录还在——你还能通过/key/info查它的花费,还能看到它关联的user_id/team_id/metadata。- 万一是误判(比如只是某个应用配错了导致的异常调用量),
/key/unblock就能恢复,不用重新签发和重新分发。
止血用 block,清理留到事后。 这个顺序反过来,你会同时失去证据和后悔药。
收益二:能限定模型白名单与预算
虚拟 key 不只是”一把可以单独吊销的凭证”,它还是一个策略载体。LiteLLM 的 /key/generate 接口签发 key 时可以带上这些字段(字段名逐字来自官方文档):
| 字段 | 类型 | 用途 |
|---|---|---|
models | 数组 | 这把 key 允许调用的模型白名单 |
max_budget | 浮点 | 花费上限,单位 USD |
tpm_limit | 整数 | 每分钟 token 数上限 |
rpm_limit | 整数 | 每分钟请求数上限 |
duration | 字符串 | 有效期,官方示例值如 "30min"、"30d" |
user_id / team_id | 字符串 | 关联到用户 / 团队 |
metadata | 对象 | 自定义元数据 |
aliases | 对象 | 模型名映射 |
官方文档里的签发示例长这样:
curl 'http://0.0.0.0:4000/key/generate' \
--header 'Authorization: Bearer <master-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"models": ["gpt-3.5-turbo", "gpt-4"],
"metadata": {"user": "email@example.com"}
}'
从安全角度看,models 和 max_budget 这两个字段的价值被严重低估了。
models 白名单把损失从”任意模型”压到”这几个模型”。 一把只能调便宜模型的 key 泄漏了,攻击者能烧的钱是有天花板的;一把什么都能调的 key 泄漏了,他会挑最贵的那个模型往死里跑——毕竟花的不是他的钱。所以签发时的默认姿势应该是只给这个调用方当前真正需要的模型,需要新模型时再改,而不是一次性全给。
max_budget 把”持续烧钱”变成了”烧到上限就停”。 回到开头那个场景:网关失陷之后你要到月底才发现。如果每把下发的 key 都有预算上限,攻击者最多烧掉这把 key 的额度,然后就调不动了——而”某把 key 突然把额度烧完了”本身就是一个会主动触发的告警信号。这等于把一个隐蔽的、持续的失血,变成了一个会响的警报。
tpm_limit 和 rpm_limit 是同一个思路的时间维度版本:它们不减少总损失,但减慢损失速度,给你留出发现和处置的时间窗。
duration 也值得一提:给临时用途(外包、演示、一次性数据处理)签发的 key 就该带有效期,到期自动失效。人是会忘记回收的,机制不会。
收益三:能定位是谁泄漏的
这是最容易被忽略、但事后最救命的一条。
如果全公司共用一把原生 key,某天它出现在某个公开的地方,你唯一能做的判断是”泄漏了”。谁泄漏的、从哪条路径出去的、还有没有别的 key 走了同一条路径——全都不知道。 那你就无法修复根因,只能轮换完祈祷不再发生。
如果每个调用方一把独立 key,并且签发时带上了 user_id / team_id / metadata,那么泄漏的那把 key 直接指向具体的人或团队。你能顺着问出来”这把 key 你放哪儿了”,然后发现是某个仓库、某个 CI 配置、某个文档——根因就定位到了。 修掉这条路径,才是真正的止损。
metadata 这个字段建议养成习惯用起来:签发时把用途、负责人、申请日期这类信息写进去。签发时多花十秒,出事时省几小时。
四、审计与留痕:以及一条必须讲清楚的合规边界
「谁在什么时候调了什么」要能查——这句话谁都会说。但落地时有一个大多数文章不讲的陷阱。
全量留存请求体,本身就是风险
LLM 网关的请求体里有什么?用户输入的原文。而用户输入的原文可能包含:客户姓名电话、内部合同条款、代码片段、医疗或财务信息……你把这些东西全量落盘并长期保存,就等于你自己制造了一个新的高价值数据库。
而且这个库通常保护得比正经业务库差得多——因为大家心理上把它当”日志”,日志嘛,权限松一点,保留久一点,谁查问题谁都能看。结果就是:你为了安全做的审计,本身成了最大的安全隐患。同时它还是个合规隐患,因为你可能在没有明确告知和授权的前提下,长期存储了用户或客户的个人信息。
所以留痕不能是”全都存下来”,得分层。
分层留痕的做法:指标全量 + 样本采样 + 敏感字段脱敏
第一层,指标全量。 每一次调用都记,但只记不含内容的元数据:
- 时间、调用方标识(对应虚拟 key 的
user_id/team_id) - 调的哪个模型、走的哪个上游
- token 用量、耗时、状态码、是否命中重试或回退
- 来源 IP(如果反向代理层能拿到)
这一层必须全量,因为它就是你回答”谁在什么时候调了什么”的主体,而且它天然不含敏感内容,可以长期保留、可以给较多人查询、可以直接拿来做异常检测和成本归因。前面说的”某把 key 把额度烧完了”这种告警,就建在这一层上。
顺带一提,网关自身的花费追踪能力也属于这一层。LiteLLM 官方说明里,花费追踪是通过数据库在 key / user / team 三个层级自动进行的,/key/info 可以查某把 key 的花费——这些天然就是不含内容的聚合数据。
第二层,样本采样。 请求体和响应体按小比例采样保留,用途是排查问题(“为什么这类请求总是失败”这种事,看不到内容就没法查)。采样意味着:
- 保留量可控,风险敞口小;
- 保留期要短,到期自动删除,别搞”先留着万一以后有用”;
- 访问要单独授权,不跟第一层的指标查询共用权限。
第三层,敏感字段脱敏。 采样保留的内容在落盘前过一遍脱敏——手机号、邮箱、身份证件号、银行卡号这类有明确格式的,用规则替换掉;结构化请求里已知的敏感字段直接置空。
脱敏要在写入之前做,不是写入之后再跑清洗任务。写入之后再洗,中间那段时间明文已经在磁盘上了,而且备份可能已经带走了一份。
关于具体用什么工具做脱敏、日志存哪儿、保留多久——这些取决于你所在行业的合规要求和公司的数据分级制度,以你们法务/合规团队的口径为准。这里能给的只有一条通用原则:审计的目的是能追溯,不是能复原。 能追溯到”哪把 key、什么时候、调了哪个模型、花了多少”就已经解决了 90% 的问题,剩下 10% 用采样加脱敏去覆盖,不值得为它建一个明文内容库。
五、默认配置的坑
自部署的软件都有这个问题:为了让人五分钟内跑起来,默认配置往往是”最方便”而不是”最安全”的。跑起来之后大多数人就不再回头看了。
初始账号:建立后立刻改强密码
首次启动后按页面提示创建管理员账号,建好之后第一件事就是设置一个强密码,并且这个密码不要和你其他任何服务重复。
这里有一条必须明说的边界:本文不给任何默认账号密码。 New API 的官方 README 并未明确给出默认管理员账号密码,网上流传的那些版本无法核实。写错一个默认凭证会造成两种事故——要么读者以为不存在而放着不管,要么读者拿着一个错的去改从而以为自己已经改过了。所以这里只能给流程:以首次启动时页面的实际提示与官方文档为准。
反过来说,你自己部署完之后应该主动去验证一件事:用一个未登录的浏览器(无痕窗口就行)访问你的管理后台地址,看看在不提供任何凭证的情况下能看到什么。这个动作花不了两分钟,能发现绝大多数”以为关上了其实没关”的问题。
关掉你不需要的开放功能
自建网关通常带一整套面向”对外提供服务”的功能:用户自助注册、邀请码、充值、第三方登录等等。如果你只是内部团队自用,这些一个都不需要开。
判断标准很简单:问自己”这个功能允许一个我不认识的人做什么”。如果答案不是”什么都做不了”,就关掉它。开放注册尤其要关——它意味着任何能访问到你后台页面的人都能给自己建个账号,然后你的攻击面就从”猜密码”变成了”合法用户的越权尝试”,后者难防得多。
具体哪些开关、在哪个菜单,各版本界面会变,以你部署的那一版的实际页面与官方文档为准。
六、升级与备份:备份里有你全部的 key
最后这一节短,但漏掉会很痛。
升级前先备份数据库。 自建网关的所有状态——用户、虚拟 key、配额、用量记录——都在数据库里。升级涉及表结构变更时,出问题就是全量丢失。备份的成本是几分钟,不备份的成本是重建整个 key 体系并通知所有调用方换 key。
然后是关键的一句:把备份本身当作机密对待。
想想备份文件里有什么:网关存的全部上游 key。这个文件的敏感等级和网关本身完全一样,甚至更高——因为网关至少还有登录和权限,而一个 dump 文件是纯明文的、可以被随便复制的。
我见过的典型错误是:数据库备份自动传到一个对象存储桶里,那个桶的权限是”团队可读”,因为它同时还放着别的没那么敏感的备份。这等于把你的密码库放进了一个公共文件夹。
正确的处置:
- 备份存储位置单独设权限,访问人数压到最少;
- 备份文件加密后再上传,密钥和备份分开存放;
- 定期做一次恢复演练——没验证过能恢复的备份不算备份;
- 旧备份按保留策略清理,别无限堆积。每一份留着的旧备份,都是一份还在生效的 key 集合。 如果你在两个月前轮换过上游 key,那两个月前的备份里存的是旧 key——但如果你只是”轮换”而没有在上游把旧 key 作废,那份旧备份依然是能用的。
顺带说,升级本身也建议留一条回退路径:备份 + 记录当前运行的镜像标识,出问题能退回去。具体的版本与镜像 tag 以官方仓库 README 当次为准,这里不给任何具体版本号。
上线前检查清单
按顺序过一遍,每条都能回答”是”再上线:
- 管理后台不能从公网直接访问——要么完全在内网,要么前面有一层独立于网关自身的访问控制。用无痕窗口实测验证过。
- API 端点与管理端点走不同的暴露策略,端口映射在 compose 层面就限制了绑定地址,而不是只靠防火墙规则兜着。
- 全链路 HTTPS,凭证不走明文;反向代理已就位。
- 上游 key 与
SQL_DSN、REDIS_CONN_STRING通过环境变量或密钥管理服务注入,确认不进镜像、不进 git、不进 shell history。 SESSION_SECRET用足够长的随机值,多节点部署时各节点一致;已评估是否给CRYPTO_SECRET单独设值而不是继续走默认继承。- 数据库账号是专用账号、最小权限,不是 root;Redis 有独立凭证。
- 下发给所有调用方的都是虚拟 key,没有任何一个业务方持有上游原生 key;每把 key 都带了
models白名单、max_budget,以及能定位到人的user_id/team_id/metadata;临时用途的 key 带了duration。 - 审计分层已生效:指标全量落盘,请求体只做采样并已在写入前脱敏,两层的访问权限是分开的。
- 初始管理员已改强密码,用不到的开放功能(自助注册等)已关闭。
- 数据库备份已跑通并演练过恢复,备份存储权限单独收紧、文件加密,旧备份有清理策略。
最后一句话概括这整篇:网关不是基础设施里普通的一环,它是你所有上游凭证的单点。 按守护密码库的标准去部署它,而不是按守护一个内部小工具的标准。
延伸阅读:聚合网关的密钥与合规安全 讲密钥分层与合规的整体框架;New API 自部署 讲部署本身怎么落地;API Key 轮换实战 讲泄漏之后五分钟内怎么换掉;访问密钥管理 讲日常的 key 生命周期管理。
本文涉及的参数名与接口路径均来自各项目官方文档与仓库 README,版本会变,落地时以你所部署版本的官方文档当次为准。文中不提供任何默认管理员凭证——首次启动请以页面实际提示与官方文档为准。