本地或内网跑了 vLLM,接口默认是裸的——谁都能调,顺手一调还能把你显卡占满。要接给同事、接进 Dify 这类上层工具,或者只是不想被同网段的人蹭,都得先加个 api_key。这篇讲清 vLLM 的 key 是怎么设的、设完客户端怎么改、什么场景下这层鉴权其实不够用,最后给一份暴露到内网前该走的检查顺序。
先明确:vLLM 默认没有鉴权
vLLM 起服务的时候,默认是不带任何身份校验的。它对外暴露的是 OpenAI 兼容接口,只要能访问到那个 IP 和端口,谁都能发请求、谁都能调模型。这不是漏洞,是它默认按「内网自用工具」来设计的。所以要不要加 api_key,本质上不是安全问题,而是「这个端口打算给谁看」的问题。
很多人以为不加 key 的风险是被入侵、被拖库。实际更常见的情况是:端口一开,隔壁网段的扫描器、云厂商的安全扫描、甚至同事随手写的一个脚本,就把你的服务当成了免费算力。请求一多,显存和并发槽位被占满,你自己的业务反而排队等不到结果。表现就是「明明没人用,怎么这么慢」。
- 默认不鉴权
- 不加 --api-key 时,vLLM 不校验 Authorization 头,任何来源都能调用
- 真正的代价
- 不是数据被偷,而是算力被白嫖,并发被挤占,自己的请求超时
- 判断的分界线
- 监听地址是 localhost 还是 0.0.0.0,决定了要不要加这层 key
| 部署方式 | 要不要加 api_key | 说明 |
|---|---|---|
| 只在本机 localhost 自测 | 可以不加 | 只有本机能连,外部扫不到,省一步配置 |
| 绑定到局域网地址给同事用 | 建议加 | 同网段任何设备都能扫到端口,容易被顺手调用 |
| 映射到公网或云主机 | 必须加 | 公网扫描是常态,裸奔等于把算力放到门口 |
| 前面已有网关做鉴权 | 可以省略 | 但要确认网关真的拦住了,而不是只做了转发 |
- 先看启动命令里的 host 参数:是 127.0.0.1 还是 0.0.0.0;
- 再看端口有没有通过防火墙、安全组或反向代理暴露出去;
- 然后问自己一句:这个地址,除了我,还有谁能 ping 到;
- 如果答案是「不止我」,那就该加 key,或者把鉴权挪到前面的网关;
- 加完 key 之后,用不带 Authorization 头的请求验一次,确认真的被拒。
有一种情况确实可以不加:你已经把鉴权交给前面的 Nginx、API 网关或者服务网格,vLLM 只在内网监听、只接受网关转发过来的流量。这时候在 vLLM 这层再配一遍 key 属于重复劳动。但前提是想清楚「谁在管」——网关的规则是谁写的、改配置要不要过审、内部服务之间走不走鉴权。如果这几条没人答得上来,那宁可 vLLM 自己带一层 key,也别把安全全押在一个没人维护的网关配置上。
还有一个容易被忽略的点:加了 key 只是挡住了「不知道 key 的人」,挡不住「知道 key 的人」。如果是多人共用一把 key,谁调了多少、谁把并发打满了,你依然查不出来。这种场景下要考虑的不是加不加 key,而是要不要按人分配 key、以及在网关层做限流。具体到各家托管服务和自建方案的差别,可以到 模型资料库(/models) 对照着看,那边按接入方式整理过。
接着往下看确定要加 key 之后,下一步是选怎么加、加在哪一层。站内把自建和走网关两种做法分开讲过,可以对着自己的部署方式挑一条路径。→起服务时怎么把 key 挂上去
挂 key 这件事只在启动命令里做一次,核心就是给 api_server 加一个鉴权参数,值填你自己临时生成的随机串。别再手敲「123456」或者「mykey」这种,等于没开鉴权。生成完存到环境变量或者密码管理器里,启动时引用它,别把明文写进 shell 历史。
- 生成随机串用 openssl rand -hex 32 输出一串十六进制随机字符,或者用 Python 的 secrets.token_urlsafe(32) 拿一串 URL 安全字符。长度往 32 字节以上靠,别自己敲键盘凑。
- 存进环境变量把结果赋给一个变量,比如 export VLLM_API_KEY=$(openssl rand -hex 32)。这样命令里引用的是变量名,不清屏也能避开历史记录里躺明文。
- 启动时挂上去在 vllm serve 或 api_server 的启动命令里加上对应的 api-key 参数,值写 $VLLM_API_KEY。具体参数名和可用取值不同版本会增删,去站内的 vllm api_server 参数清单对当期的写法。
- 验证鉴权是否生效起服务后,先不带 Authorization 头打一次接口,应当被拒;再带上 Bearer 加那张 key 打一次,应当正常返回。两次结果不一致才算挂对了。
| 做法 | 适用情况 | 不适用情况 |
|---|---|---|
| 命令行直接填随机串 | 本地临时调试、单机验证 | 命令会进历史记录或共享终端 |
| 环境变量引用 | 常规部署,key 不希望出现在进程参数明文里 | 环境本身不可信、变量会被日志带出 |
| 启动脚本里从密钥文件读 | 多实例复用同一套部署流程 | 需要按人区分 key、按 key 统计 |
| 外面套一层网关 | 要按人发 key、按 key 限流或做审计 | 只是单机自己用,图省事 |
这里有个容易踩的坑:vLLM 这层鉴权是单 key 语义,服务端只认「有没有这张 key」,不区分是谁在用,也不给你做按 key 的配额或限流。所以如果你要按人发 key、按 key 看用量或者按 key 限速,别在这层硬凑,应该在外面加一层网关,由网关分发多把 key、做统计和限流,vLLM 只管模型推理那部分。
- 钥匙生成后先存好再启动,别启动完才回头找
- 验证分两次:不带 key 应被拒、带 key 应通过
- 多人共用一套服务时,先在网关侧规划 key 的分配方式
- 不同版本参数名可能不同,以站内当期参数清单为准,别照抄旧命令
- key 一旦怀疑泄露,直接换一串重启,成本比排查低
改完怎么确认鉴权真的生效了
改完参数重启服务,别急着接业务,先花两分钟把三件事按顺序跑一遍。很多人卡在「看起来能通」就收工了,结果鉴权其实没生效,谁都能裸调。下面这套顺序的关键是:先证明不带 key 会被拦,再证明带对 key 能放行,最后证明带错 key 还是被拦。
- 不带 Authorization 头请求直接 curl 模型列表接口,什么头都不加。预期是 401 或等价的未授权响应,绝不能返回正常数据。如果这里就正常返回了,说明鉴权根本没挂上,后面的测试都不用做了。
- 带正确的 Bearer 头再请求一次把启动时配置的那个 key 拼成 Authorization: Bearer 再打一遍同一个接口。预期返回模型列表,说明服务确实读到了 key 并且比对通过。这一步能通,才说明链路是活的。
- 换一个错的 key 再打一遍把 key 改成随便一串明显不对的值,第三次请求同一接口。预期仍然是被拒。这一步是为了排除「服务压根没校验、只是网络刚好能连」这种假阳性。只有前两步对、第三步也拦得住,才算真的生效。
# 1) 不带头
curl -i http://127.0.0.1:8000/v1/models
# 2) 带正确的 key
curl -i -H "Authorization: Bearer 你的KEY" http://127.0.0.1:8000/v1/models
# 3) 带一个错的 key
curl -i -H "Authorization: Bearer wrong-key-test" http://127.0.0.1:8000/v1/models| 请求方式 | 预期结果 | 不符合说明什么 |
|---|---|---|
| 不加 Authorization 头 | 401 / 未授权 | key 没生效,或接口没走鉴权中间件 |
| 带正确 Bearer 头 | 正常返回模型列表 | key 没被正确读入,或头格式不对 |
| 带错误 Bearer 头 | 仍然 401 / 未授权 | 校验逻辑没跑,可能是代理放行或参数没解析 |
三步里任何一步不符合预期,排查顺序是固定的:先回去翻启动日志,确认启动参数里那个 key 有没有被解析进去、有没有报参数拼写错误;再看服务前面是不是挂了反向代理,比如 nginx 或网关,它们有可能把 Authorization 头改写或直接丢掉。顺序别倒过来先怀疑客户端。
下一步鉴权确认没问题后,去站内把服务暴露方式和调用示例对一遍,避免上线后才发现端口或路径不对。→客户端和上层工具怎么改
先说结论:绝大多数兼容 OpenAI 协议的客户端,配置项就两个——base_url 和 api_key,模型名填你启动 vLLM 时用的那个名字(就是 --served-model-name 那块的东西,客户端别自己猜)。先把这两项填对,比研究客户端源码快得多。
| 客户端里的字段 | 填什么 |
|---|---|
| Base URL / API 地址 | 你的服务地址,带不带 /v1 两种都试一次 |
| API Key / 密钥 | 你启动时设的那个 key,没设就得先设一个 |
| Model / 模型名 | 部署时用的 served-model-name,照抄 |
base_url 带不带 /v1 是最高频的坑。有的客户端自己会拼 /v1/chat/completions,你多写一个 /v1 就变成 /v1/v1/...;有的客户端要求你必须写全。没法一眼看出来的话,两种各试一次,哪个返回正常用哪个,别去翻文档猜。
- 先验证服务本身通不通用 curl 直接打一次 chat/completions,确认服务能返回内容,排除是服务端问题还是客户端配置问题
- 填 base_url 两种写法先按带 /v1 填一次,报错就改成不带 /v1 再试,两次之内基本能定位
- 填 api_key 和模型名key 用你启动参数里设的那个,模型名严格照抄 served-model-name,不要用模型文件路径名
- 确认返回内容随便问一句短问题,能稳定返回就说明链路通了,再接到实际业务里
接 Dify、Open WebUI 这类平台时,填的是同一个 key,但注意这些平台会把 key 存在它自己的服务端。如果你在公网能访问的测试实例上填了生产 key,等于把 key 放到了别人能碰到的地方。测试实例就用一个单独的、权限受限的 key,别图省事复用。
- 平台侧存 key 是常态,先确认这个实例谁能访问
- 测试实例单独发一个 key,别和生产共用
- 确认平台能不能限制模型范围,能限就限
- key 一旦怀疑暴露,直接换掉重启服务,别只改客户端配置
还有一种情况是客户端根本不走 OpenAI 协议,比如只认自己的私有格式。这种不是配置问题,是协议不匹配,硬调字段没用,要么换个客户端,要么在前面套一个协议转换的转发层。判断方法很简单:看它文档里有没有「兼容 OpenAI」这类字眼,没有就别浪费时间试。
key 本身怎么管
先说结论:vLLM 自带的这层 key 只是「进门口令」,不是账号体系。它的作用是让请求方在调用时多带一个字段,服务端对一下是否匹配,匹配就放行。它不知道谁是谁、调了多少次、该扣多少钱。所以「怎么管」的重点不是把它当机密文件供起来,而是别让它出现在不该出现的地方,并且换的时候别把线上打挂。
三个最常踩的坑,按出现频率排
| 坑 | 典型表现 | 为什么麻烦 |
|---|---|---|
| 写进代码仓库 | key 直接硬编码在启动脚本或客户端里,push 上去 | 仓库可能公开,历史提交删不干净,协作者人手一份 |
| 写进前端 | 浏览器里直接 fetch 模型接口,key 放 JS 变量 | 打开开发者工具就能看到,等于公开 |
| 留在命令行历史 | curl 时把 key 拼在参数里,之后按上箭头就复现 | 本机 history、终端日志、同事录屏都可能留存 |
正确的存放位置:环境变量或本地 .env
- 服务端读取环境变量启动 vLLM 时从环境变量取 key,而不是写死在命令里。可以先用一行命令确认变量确实注入了,比如 echo 检查前缀,别打印完整值。
- 客户端也用环境变量客户端代码统一从环境变量读,本地开发放 .env 文件。写完用一次实际请求验证:把环境变量临时改错,请求应该被拒,说明确实是从变量读的。
- 把 .env 加进忽略清单在仓库的忽略文件里加上 .env 以及任何带 key 的本地配置文件。加完用版本控制的状态命令确认它没有被跟踪,再提交一次看看它是否出现在变更列表里。
- 给一份 .env.example仓库里只保留字段名和占位符的示例文件,真实值永远不进仓库。这样新同事知道要配哪几个变量,也不会误提交。
- 服务端:环境变量注入,启动命令里不出现明文 key;
- 客户端:环境变量或 .env,代码里只出现变量名;
- 仓库:.env 进忽略清单,只提交 .env.example;
- 终端:不用带 key 的 curl 直接贴进聊天窗口,需要分享就先把 key 替换成占位符;
- 容器:用密钥管理或挂载文件,别把 key 写进镜像层。
轮换节奏:什么时候必须换,怎么换不中断
这层 key 没有细粒度审计,泄漏了也未必能第一时间发现,所以判断标准要粗一点、往严里靠:只要有人离职、key 被共享给过外部、或者曾经贴在群里 / 工单里 / 截图里,就直接换,不要评估「应该没事」。定期轮换的频率站内没有统一建议,可以到 指南中心(/guides/) 看通用的密钥轮换思路,再结合你们自己的规模定。
- 挑一个低峰切换窗口服务端和客户端必须同时改,中间必然有一段不一致的时间。选调用量最低的时段,提前在群里说一声。
- 先服务端同时接受新旧两把 key让服务端临时认可两把 key,此时旧客户端仍然能通。验证方式是分别用新旧 key 各发一次请求,两个都应该成功。
- 再逐台改客户端按机器或按服务分批更新客户端配置并重启。每改完一批,就用监控或日志确认没有认证失败堆积。
- 最后摘掉旧 key确认所有客户端都在用新 key 后,服务端移除旧 key。摘掉后再用旧 key 发一次请求,应该被拒,这才算收尾完成。
一台机器跑多个实例:一个实例一把 key
如果同一台机器上起了多个 vLLM 实例,或者一个实例服务多个上游业务,别图省事共用一把 key。每把 key 对应一个明确的调用方,出问题时看服务端日志就能定位到是谁在打,而不用猜。代价只是多配几个环境变量,收益是排查时间从「翻半小时日志」降到「看一眼就知道」。
| 场景 | key 怎么分 | 出问题时怎么定位 |
|---|---|---|
| 同机多实例 | 每个实例一把独立 key | 看是哪个实例的日志出现认证记录 |
| 一个实例多个上游业务 | 每个业务一把 key | 按 key 区分调用来源,定位到具体业务线 |
| 测试与生产混用 | 测试单独一把,绝不复用生产 key | 测试打满也不影响生产,也便于随时作废 |
它做不到什么,别把它当正式服务的安全边界
如果你的模型是要对外提供正式服务的,这层 key 不够用:它没有按量计费、没有按次配额、没有细粒度审计,也无法在单个调用方异常时只掐那一个。正式对外时应该在前面加一层网关或鉴权代理,把计费、限流、审计交给那一层来做,vLLM 这层 key 只当内网的第二道门。反过来,如果只是小团队内网自用、调用方都是自己人,那这层 key 加上环境变量管理就够了,不必上更重的方案。
要不要暴露到公网:先过这张清单
vLLM 自己那个 api_key 属于最内层的一道门,挡的是「谁拿着 key 谁能调模型」,它不管端口是不是被扫、也不管别人是不是拿它当免费算力。所以在配 key 之前,先把「这个服务到底要不要给外面看」想清楚,顺序反了后面会一直补漏洞。
- 先问能不能只在内网用能内网解决就不要做端口映射。让调用方和 vLLM 在同一内网或同一容器网络里,通过内网地址直连;验证方式是换一台外网机器去连这个地址,连不上才说明没暴露。
- 跨网络就上隧道或 VPN确实要跨公网访问,优先用 WireGuard、Tailscale 这类隧道,或者公司已有的 VPN,让调用方先进内网再连 vLLM。验证方法是看服务监听地址仍绑在内网网卡上,公网侧扫不到这个端口。
- 必须开端口时,前面加反代Nginx/Caddy 这类反代放在最前面做 TLS 终止,同时挂上限流和 IP 白名单,vLLM 只监听回环或内网。验证方式是直接从公网访问 vLLM 的裸端口,应当连不通,只能走反代的域名或入口。
- 只放业务需要的路径反代里只转发业务真正用到的那几条前缀,把管理类、调试类、指标类端点挡在外面。验证方式是在反代上逐条列白名单,再拿一台不在白名单的机器试那几个被挡路径,确认返回拒绝而不是正常响应。
- 暴露前后各量一次延迟上公网之前,先用站内的接口测速页面记一条延迟基线;暴露之后再量一次。如果同一个请求的延迟明显变差、抖动变大,多半是有人蹭你的算力或者端口被扫到了,先去查访问日志的来源 IP。
| 暴露方式 | 谁来挡第一层 | 什么时候不适用 |
|---|---|---|
| 仅内网地址 | 网络本身就不通 | 调用方和模型不在同一网络时 |
| 隧道 / VPN | 隧道或 VPN 的鉴权 | 对接方只支持公网 HTTP 时 |
| 反代 + 白名单 | TLS、限流、IP 白名单 | 调用方 IP 不固定又做不了鉴权时 |
| 直接映射公网端口 | 只有 vLLM 的 api_key | 任何生产或长期在线的场景 |
- 服务是否只监听内网或回环地址,公网侧能否扫到端口;
- 反代是否做了 TLS、限流和 IP 白名单三件事;
- 管理类、调试类、指标类端点是否确认没被一起转发;
- 暴露前后的延迟基线是否各测过一次并做了对比;
- 访问日志是否留了来源 IP,出问题时能不能回溯;
- key 是否按期轮换,且没有在客户端或前端里硬编码。
这张清单里任何一条答不上来,就先别开公网。先把暴露面收回去,再回头配 api_key 才有意义;真需要公开访问的,去站内接口测速页面 /speed-test 先记一条延迟基线,暴露后再对着同一入口复测,数字变化会直接告诉你有没有被人蹭。关于 key 本身怎么设、怎么轮换,不在这一节展开。
排查顺序:401 之外的坑
带上正确的 key 还报 401,别急着换 key,九成是请求头写法的问题。检查 Authorization 这一行是不是严格的「Bearer + 一个空格 + 你的 key」,大小写、空格数量、有没有多余引号都会让服务端认不出来。用 curl 或 SDK 都先打印一遍实际发出去的 header 再判断。
- 拼写:Bearer 首字母大写,后面必须有一个半角空格,不能是冒号或等号
- 粘贴:从配置文件或环境变量复制时容易带上换行、引号、末尾空格
- 前缀:有的客户端会自动加 Bearer,你再手写一遍就变成两个 Bearer
- 位置:确认放在请求头而不是查询参数或请求体里
- 编码:key 里若有特殊字符,注意 shell 转义和配置文件引号处理
- 打印实际请求头用 curl 加 -v,或让 SDK 打开日志,看到真正发出去的那行 Authorization;比对是否等于「Bearer 空格 key」。
- 确认参数位置如果服务根本起不来或参数像被忽略,检查启动命令里参数是放在子命令之前还是之后;有些写法要求放在子命令之后才生效,放错位置会被静默丢弃而不是报错。
- 通了但慢,先分离鉴权能正常返回但延迟高,和 key 基本无关。先看并发数是否被打满、显存是否吃紧,到站内显存与并发相关的页面对一遍自己的配置。
- 完全连不上,先查端口和绑定连接被拒或超时,先确认监听端口和绑定地址是否对得上(例如只监听了本机地址却从外部访问),再回头怀疑鉴权。
| 现象 | 最可能的层面 | 先查什么 |
|---|---|---|
| 正确 key 仍 401 | 请求头格式 | Bearer 与 key 之间的空格、是否重复加前缀 |
| 服务起不来/参数无效 | 启动命令 | 参数是否放在子命令之后,是否被静默忽略 |
| 能通但很慢 | 资源与并发 | 并发上限、显存占用,对照站内并发与显存页面 |
| 完全连不上 | 网络与绑定 | 端口是否开放、绑定地址是否只限本机 |
| 换 key 后依旧 401 | 环境不一致 | 客户端实际读取的 key 与配置文件是否同一个 |
常见问题
vLLM 设置 api_key 之后,Ollama 那边的做法能照搬吗?
不能照搬。两者的鉴权机制不是一套东西,环境变量名、参数位置都不一样。如果你同时跑着两个框架,建议每个都单独查一遍对应文档,别凭印象改配置,改错了往往表现为「连不上」,很难第一眼看出来是鉴权还是网络问题。
只跑单机 localhost,还有必要设 key 吗?
单机自测阶段可以先不设,省一步。但只要你打算把它接进别的工具、或者以后可能要给别人调,建议一开始就设上,后面加客户端配置时习惯已经养成,不用回头再改一遍客户端和服务端。
一个 key 能不能分给多个人用?
技术上可以,但这层鉴权本身不区分调用者,没法按人看用量、也没法单独吊销某个人。如果确实要按人管,正确做法是在 vLLM 前面加一层网关,由网关发多把 key 并做配额,vLLM 只认网关过来的那一个。
设完 key 之后原来能用的客户端挂了,怎么快速定位?
先把客户端和鉴权解耦验证:用命令行带正确 header 直接请求一次模型列表,能通说明服务端没问题,问题在客户端配置;不能通说明服务端没读进参数。这样一次就能把范围缩到一半,比反复改配置快。
这层 key 泄露了会有什么后果?
最直接的后果是别人用你的卡跑推理,你的业务请求被挤慢甚至超时,显存和算力被白占。因为它不带计费能力,损失以资源为主。发现泄露就立刻换 key 并重启服务,同时检查有没有别的东西通过这个入口被访问到。