1. 先盘点现状:vLLM 镜像里的端口值和一把“简易钥匙”
1.1 镜像启动后,API 默认在 8000 端口裸奔
先说实话,很多人在本地把 vLLM 镜像跑起来,一条命令下去模型能出字了,就以为“上线了”。但这时候服务的 API 是默认监听在0.0.0.0:8000的,也就是说,只要主机有公网 IP 或者防火墙没拦住 8000 端口,任何能访问到这个地址的人,都能往你的推理服务里丢请求。
我在实际部署里见过不止一次的情况是:一个团队的 GPU 服务器刚配好公网 IP,还没来得及套任何防护,第二天一看账单,令牌被刷了几万次,模型响应慢到没法用。原因无非是公网扫描器会把常见端口翻个遍,找到 8000 端口返回的是 OpenAI 兼容的响应格式,那基本等于告诉全世界“这里有个免费的 LLM 接口”。
之前有人问我,vLLM 默认端口到底是多少?答案是 8000。用--port可以改,但我通常不建议改成一个冷门端口来“隐藏”,因为这只挡得住不会扫端口的小白,挡不住真正想薅羊毛的人。更靠谱的思路是:默认裸奔状态必须做点什么,哪怕是最简单的一层认证。
1.2 用 --api-key 或 VLLM_API_KEY 开启最基础的 Bearer 校验
如果你只是想让服务不被陌生人随便调用,vLLM 自己提供了一个很轻量的开关:--api-key。新版镜像也支持用环境变量VLLM_API_KEY来设置同等效果。
从命令行启动时,可以这样写:
docker run --runtime nvidia --gpus all \ -v ~/.cache/huggingface:/root/.cache/huggingface \ -p 8000:8000 \ -e VLLM_API_KEY="sk-prod-xxxxx" \ --ipc=host \ vllm/vllm-openai:latest \ --model /models/your-model \ --served-model-name your-model启动之后,你在本地用 curl 测试:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-prod-xxxxx" \ -d '{"model":"your-model","messages":[{"role":"user","content":"你好"}]}'不带Authorization头,或者带一个错误 key 的请求,会直接返回 401。虽然这个校验逻辑本身不复杂,但对个人项目、内部工具的防护来说,已经能拦住绝大多数“随手扫端口”的流量。
1.3 只靠一把钥匙,还有哪些不放心
很多人以为加上VLLM_API_KEY就万事大吉,等到真的开始多人协作、多业务共用一个服务时,痛点就出现了。
首先是一把钥匙所有人共用,你根本不知道某个请求到底来自哪个同事、哪个业务线。其次是钥匙一旦泄露,没法单独吊销,只能改全局 key 然后通知所有人更新配置。更麻烦的是,vLLM 原生的 key 校验是“存在性校验”级别的,也就是说,它只会判断这个请求带上的 Bearer token 是不是和你设的一致,并不支持给不同人分配不同 key、也做不了按用户维度的限流和配额统计。
所以我的建议是:如果你只是在本地调试、或者服务只跑在内网且调用方都是可信的,那VLLM_API_KEY够用;但只要这个服务要上公网、有多个调用方、或者流量有一定规模,就得往上一层考虑。
提示:
--api-key和VLLM_API_KEY本质上是同一层防护,别同时配两个不一样的 key,容易把自己绕晕。
2. 进阶方案:JWT 鉴权插件,给多个调用方发不同“工牌”
2.1 --auth-plugin 的工作方式
vLLM 的 OpenAI 兼容服务入口留了一个自定义鉴权接口,叫--auth-plugin。通过这个参数,你可以挂一个 Python 类进去,vLLM 在处理每个请求前会调用这个类的方法做校验。
具体到代码层面,你需要写一个类,继承 vLLM 提供的鉴权基类,并实现authenticate方法。这个方法的入参是 FastAPI 的 Request 对象,返回值需要包含用户 ID、权限范围等信息。
它和--api-key最大的区别在于:--api-key是“对暗号”,所有请求同一个 key;而--auth-plugin可以让你在鉴权逻辑里自由发挥,比如解析 JWT、查数据库、判断该用户的配额还剩多少。
2.2 一个 JWT 校验插件长什么样
下面是一个很常见的实现思路,我没有把完整业务逻辑贴出来,但骨架大概是这样:
import time import jwt from vllm.entrypoints.openai.serving_auth import AuthResult, UserAuth class MyJWTPlugin(UserAuth): def __init__(self, config): super().__init__(config) self.jwt_secret = "你的签名密钥" async def authenticate(self, request) -> AuthResult: token = request.headers.get("Authorization", "").replace("Bearer ", "").strip() try: payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"]) except jwt.ExpiredSignatureError: # 过期了 return AuthResult(user_id=None, scopes=None) except jwt.InvalidTokenError: # 伪造的令牌 return AuthResult(user_id=None, scopes=None) # 校验通过,返回用户身份 return AuthResult( user_id=payload.get("sub"), scopes={"chat": True} )启动时加上:
--auth-plugin my_auth_module.MyJWTPlugin需要注意,vLLM 的鉴权插件是在服务端进程内跑的,所以这个类必须能被服务进程 import 到。如果你用的是官方镜像,最简单的办法是做一个自己的镜像,把写好的鉴权文件拷进去,并把工作目录加进 PYTHONPATH。
2.3 用令牌换接口权限的常见设计
JWT 方案的好处不只是“多一把钥匙”,而是你可以把权限信息塞进 token 里。比如:
scope字段表示这个调用方能用哪些模型quota字段记录每分钟最多请求多少次exp字段直接让 token 到期自动失效aud字段可以区分这个 token 是给内部同学还是给外部客户用的
这样做的实际意义是:当你把 key 发给业务方时,不用给一个“永不过期、权限不明”的字符串,而是给一个有时效、有范围的令牌。如果某个业务方不再续费或合作停止,只要等 token 过期,或者把你这边签发的密钥轮换掉,就能批量失效。
不过 JWT 方案也有代价:你需要自己维护签发端和校验端的逻辑,而且 token 本身没法在签发后单独吊销(除非你引入黑名单机制)。所以它更适合已经有一定工程能力的团队,而不是只想“加个认证”的个人项目。
3. 生产级方案:把鉴权从 vLLM 里挪到反向代理层
3.1 为什么最终都会走到“代理层鉴权”
我见过不少团队,一开始用--api-key,后来换成 JWT 插件,再往后发现要做的越来越多:限流、审计、按用户统计、按业务线隔离、灰度发布、多模型路由……这些如果全塞进 vLLM 的进程里,一个是维护成本高,另一个是每次升级 vLLM 版本还要重新适配插件。
所以成熟一点的部署,几乎都会在 vLLM 前面加一层反向代理。这层代理可以是一个简单的 Nginx,可以是一个 API 网关,也可以用 Kubernetes Ingress 自带的认证插件。vLLM 本身只管推理,认证、限流、审计全都放在代理层解决。
这样做的核心好处是:鉴权和推理拆开了。你要升级鉴权策略,不用重启 GPU 服务;你要换推理引擎,不影响调用方的接入方式;你甚至可以同时在后面挂多个 vLLM 实例,由代理做负载均衡。
3.2 Nginx 配置:Bearer Key 校验 + 限流
如果你不想引入太重的组件,Nginx 是最快能落地的方案。先定义一个 map,把合法的 key 判断出来:
map $http_authorization $is_authorized { default 0; "~^Bearer sk-prod-[A-Za-z0-9]{16,}$" 1; }然后在 server 里做拦截:
server { listen 443 ssl; server_name llm.internal.example.com; ssl_certificate /etc/nginx/certs/tls.crt; ssl_certificate_key /etc/nginx/certs/tls.key; # 定义限流区域 limit_req_zone $binary_remote_addr zone=llm_api:10m rate=10r/s; location / { if ($is_authorized = 0) { return 401; } # 按 IP 限流,超过 20 个突发请求直接拒绝 limit_req zone=llm_api burst=20 nodelay; proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Authorization $http_authorization; } }关键点在于:proxy_set_header Authorization $http_authorization一定要把原始请求的 Authorization 头透传给后端的 vLLM。这样后端也可以校验一次,避免有人绕过 Nginx 直连 8000 端口。
如果你的 key 机制需要更复杂的校验,比如支持多个 key、或者要根据 key 找对应的用户信息,建议在 Nginx 里配合 Lua 脚本,或者干脆换一个真正的 API 网关。
3.3 网关方案:Token 生命周期、审计和按用户限流
当调用方多到“按 key 分类”都不够用的时候,就该请 API 网关进场了。我用的比较多的是基于 OpenResty 或各类云原生网关的方案。它们通常提供这些能力:
- 认证插件:直接支持 Basic Auth、JWT、OAuth2、自定义 Auth 服务
- 限流插件:可以按 IP、按 API Key、按用户维度分别限流
- 审计日志:每个请求的调用方身份、模型、token 数、耗时都记录
- 动态配置:改限流阈值、加黑名单,不用重启网关
网关层面的鉴权流程通常是:
- 客户端拿到一个服务端签发的 token 或 API Key
- 请求先到网关
- 网关校验 token,解析出用户身份
- 网关把解析出的用户信息,通过请求头传给后端 vLLM
- vLLM 专心处理推理,不再关心认证
这种模式下,vLLM 甚至可以不启用自己的--api-key,因为外部流量根本无法直接触达它。网关背后可以是内网 IP,只开放必要的端口给网关所在网段。
3.4 别忘了给代理配 TLS,否则认证等于白做
这一节我必须单独拎出来说,因为踩坑的人太多了。很多人配了 Bearer token、配了 JWT,却直接用 HTTP 对外提供服务。要知道,Bearer token 是明文放在请求头里的,如果你不用 HTTPS,那 token 在网络传输过程中就等于明文。只要有人在你和服务器之间的链路上抓包,就能直接拿到你的 key。
在 Nginx 里启用 TLS 只需要配好证书路径,并且把 80 端口的流量全转发到 443。用自签名证书虽然会引发客户端提示,但至少比明文强。生产环境还是建议用合法的受信任证书,省得调用方被安全策略拦得怀疑人生。
注意:vLLM 镜像本身不会帮你做 TLS,它默认就是 HTTP。要把 HTTPS 终止在代理层,再由 Nginx 转成 HTTP 回源到 vLLM,这是最常见的架构。
4. 多调用方接入:Key 怎么发、SDK 怎么连、日志怎么盯
4.1 设计一套简单的 Key 发放和管理流程
如果你只需要应付几个内部系统,Key 发放不用搞得很复杂,但至少要有一个记录表。我比较习惯的做法是给每个 key 增加几位“身份码”:
sk-prod-运营后台-001 sk-prod-数据分析-002 sk-test-张三-003这样做的好处是:当审计日志里出现某个 key 在疯狂调用时,你一眼就能看出是哪个业务线、哪个负责人在用,不需要去翻数据库才能定位。哪怕 vLLM 原生 key 校验不支持这种格式限制,只要代理层的正则规则够聪明,你甚至可以直接按前缀区分不同调用方的权限。
发放流程上,我建议至少包括这几个动作:
- 申请:谁要用,为什么用,大概什么量级
- 签发:生成 key,记下签发时间、关联负责人、允许访问的模型
- 通知:直接把 key 给调用方的开发负责人,不要全群转发
- 审计:定期过一遍 key 列表,把不活跃的和疑似泄露的都清掉
4.2 OpenAI SDK 与 curl 两种接入姿势
对大多数开发团队来说,他们对接 LLM 服务第一反应就是直接用 OpenAI SDK。只要 vLLM 跑的是 OpenAI 兼容接口,设置一个base_url指向你的代理地址就完事:
from openai import OpenAI client = OpenAI( base_url="https://llm.internal.example.com/v1", api_key="sk-prod-运营后台-001", ) resp = client.chat.completions.create( model="your-model", messages=[{"role": "user", "content": "今天天气怎么样"}], ) print(resp.choices[0].message.content)命令行里临时验证一个 key 是否生效,可以直接用 curl:
curl https://llm.internal.example.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-prod-运营后台-001" \ -d '{ "model": "your-model", "messages": [{"role": "user", "content": "hi"}] }'很多时候我们用 Python requests 也能直接对接。需要注意的一点是:请求体内的model字段,必须是 vLLM 启动时--served-model-name设置的那个名字,不一定是模型原始的文件名。这个字段对不上,vLLM 会直接报 model not found,跟鉴权失败是两回事。
4.3 审计日志和异常调用追踪
认证只是门槛,真正能让你睡得着觉的,是日志。我强烈建议你至少记录以下字段:
- 时间戳
- 调用方 IP
- Authorization 头里的 key(可以截断,比如只保留前几位和后几位)
- 请求的模型名
- 输入 token 数和输出 token 数
- 响应状态码
- 耗时
有了这些日志,你可以轻松回答几个高频问题:哪个 key 在凌晨三点疯狂调用?哪个接口的响应时间突然飙高?某个模型是不是在被大量测试类 prompt 刷?
vLLM 自带了一些访问日志,但如果你想按 key、按用户维度统计,建议还是走代理层记录,或者把 vLLM 的日志采集进统一的日志平台里再分析。否则每次排查都要登服务器翻日志,效率太低。
4.4 按用户维度限流与配额
鉴权的下一步是配额管理。只认证不限流,等于放一辆没刹车的车上了高速。个人项目可以用 Nginx 的limit_req按 IP 限流,但这在多人共用出口 IP 时容易误伤。更精细一点的做法是在网关层解析出 key 对应的用户或业务线,再按这个维度限流。
比如你给 A 部门分配了每分钟 20 次请求的额度,给 B 部门分配了每分钟 100 次请求的额度。当某个 key 的请求频率超过你设定的阈值时,直接返回 429 Too Many Requests。这样即使有调用方代码写了个死循环,也只会拖垮它自己的额度,不会影响其他业务方调用同一个模型服务。
5. 镜像部署避坑指南:环境变量、端口映射和配置热更新
5.1 环境变量与启动参数的优先级
我在帮别人排查问题时,经常看到一种情况:命令里写了--api-key sk-A,但服务实际用的却是sk-B。最后发现,是 Docker 启动命令里同时挂了-e VLLM_API_KEY=sk-B。虽然大部分情况二者是等价的,但当你混用时,容易产生预期外的覆盖关系。
我的习惯是统一用一个入口:如果是 docker run 临时起服务,就只写-e VLLM_API_KEY;如果是在 docker compose 或者 K8s 里管理,就用环境变量统一管理,不要把命令行的参数和环境变量混着写。否则排查起来,你都不知道要从哪里找当前生效的配置。
5.2 端口映射与镜像重启的坑
vLLM 镜像启动的时候,容器内的服务端口默认 8000,映射到宿主机可以随便指定。但有些团队为了让调用方少改配置,一直复用某个固定端口。有一天需要换机器迁移、或者升级镜像版本时,如果新容器起不来,老容器被停掉,端口被占用,就会造成短暂的服务中断。
更好的做法是:在代理层把上游地址配置成一个内网域名,而不是直接写 IP:端口。这样 vLLM 实例需要滚动升级时,代理层指向的域名可以逐步切换流量,避免“端口起不来、业务全挂掉”这种情况。
另外,我踩过的坑是:容器里面写临时文件默认可能写到容器层,一旦容器删掉,数据也没了。如果鉴权插件依赖一些本地缓存,建议把对应目录挂载成持久化卷区。
5.3 日志里别把 Token 打出来
这是个很低级但很容易犯的错。有时候为了方便调试,会在代码里把请求头打出来看一眼,打完忘了删。如果这时候日志系统被采集走,所有带着Authorization: Bearer sk-xxx的请求头就全被记录在案了。这不只是安全问题,还可能导致日志平台变成新的泄露源。
我的做法是写一个日志过滤器,把请求头里的 Authorization、Cookie 之类的敏感字段统一替换成***。同时给日志平台设置访问权限,不是所有人都有权限查生产日志。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 请求返回 401 | 没带 Authorization 头,或 key 不正确 | 用 curl 手动带 key 试一次,确认 key 没被环境变量覆盖 |
| 请求返回 404 | 模型名和 --served-model-name 不一致 | 请求体里换成服务注册时的模型名 |
| 代理层报 502 | 上游 vLLM 地址写错,或容器未就绪 | 看 Nginx error log,确认能访问到 127.0.0.1:8000 |
| 请求返回 429 | 触发了限流策略 | 查看是 IP 维度还是 key 维度限流,适当调整阈值 |
| 加了 TLS 后客户端报证书错误 | 证书链不完整或使用了自签名证书 | 检查证书配置,或把自签名证书导入客户端信任区 |
| 模型能访问但响应很慢 | 可能是鉴权插件里做了同步网络请求 | 检查插件逻辑,尽量避免在请求路径上做阻塞式调用 |
5.5 新手最容易犯的 5 个错
最后分享五个我在新手项目里反复看到的问题:
- 只给 vLLM 加
--api-key,但代理层完全没配 TLS,key 在网络里裸奔。 - 把 vLLM 的 8000 端口直接映射到公网,外部流量先于代理层触达真实服务,认证等于形同虚设。
- 用同一个 key 给所有调用方,出问题后无法定位,只能全局重置。
- 配了限流,但阈值拍脑袋写得很小,导致正常的批量任务被误杀。
- 修改鉴权配置后没有验证健康检查接口。负载均衡器和监控系统的探针可能是从内网发起的,如果不放行
/health或/metrics,服务会被误判为宕机,造成告警轰炸。
提示:vLLM 的
/health路径会返回 200 表示服务就绪,但注意它同样不应该对公网完全开放。更好的做法是让代理层只放行内部网段的探针请求,其他来源一律走完整鉴权流程。
我个人在实际部署里的体会是,vLLM 的鉴权从来不是“开一个开关”就能永远高枕无忧的事,而是一次次跟着调用方数量、业务复杂度升级的。先让服务不再裸奔,再逐步引入代理层、TLS、限流和审计日志,才是比较稳妥的路径。如果你现在的 vLLM 服务还只是停在“跑得通”的阶段,那我的建议很简单:今天就把VLLM_API_KEY配上,把 8000 端口从公网防火墙里摘掉,剩下的再慢慢打磨。