17 KiB
17 KiB
产品需求文档 (PRD)
Key-IP Sentinel:大模型 API 密钥 IP 防泄漏网关
版本:v1.1 | 状态:待开发 | 作者:算力平台管理员
一、产品背景与目标
1.1 背景
企业内部算力平台(基于 New API + vLLM 自部署)面向几百至上千名内部研发人员开放使用。用户注册账号后可自助申请 API Key,但现有机制无法防止用户将 API Key 分享给外部人员或在未授权设备上使用,存在算力被盗刷的风险。
1.2 解决方案定位
在调用方与 New API 之间部署一个独立的轻量级反向代理服务 Key-IP Sentinel,实现"首次使用即绑定(First-Use-Bind)"机制——API Key 在第一次被调用时,系统自动将该 Key 与发起调用的客户端 IP 绑定;此后该 Key 只能从绑定的 IP 发起请求,其他 IP 全部拦截。整个过程对用户无感知,无需管理员手动介入。
1.3 产品目标
- 彻底杜绝 API Key 被分享到外部或其他未授权设备上使用。
- 系统自动完成 IP 绑定,管理员只需处理少数"换 IP"的运维操作。
- 提供清晰的管理后台,支持查看绑定状态、拦截日志、手动运维。
二、整体架构
2.1 流量链路
调用方 (Client)
│
│ HTTP (80)
▼
┌─────────────────────────────────────────┐
│ Nginx │
│ 职责:路径路由 / │
│ 静态文件 / 内网鉴权 / 粗粒度限流 │
└────────────────┬────────────────────────┘
│ HTTP 内网转发
▼
┌─────────────────────────────────────────┐
│ Key-IP Sentinel App │
│ 职责:Token提取 / IP绑定校验 / │
│ 代理转发 / 管理 API / 告警 │
└───────┬─────────────────┬───────────────┘
│ │ 异步写
┌────▼────┐ ┌─────▼──────┐
│ Redis │ │ PostgreSQL │
│ (热缓存) │ │ (持久化) │
└─────────┘ └────────────┘
│
▼
┌─────────────────────────────────────────┐
│ New API (UDPI) │
│ 职责:用户鉴权 / 额度管理 / 计费 / 路由 │
└───────────────┬─────────────────────────┘
│
┌──────▼──────┐
│ vLLM 节点 │
└─────────────┘
2.2 技术选型
| 模块 | 技术方案 | 说明 |
|---|---|---|
| 反向代理 & 业务后端 | Go (Gin) 或 Python (FastAPI) | 优先推荐 Go,高并发下内存占用极低,SSE 透传天然支持 |
| 缓存层 | Redis 7+ | Token-IP 绑定热数据,TTL 7 天 |
| 持久化层 | PostgreSQL 15+ | 绑定记录与审计日志,使用 inet/cidr 原生类型做 IP 范围匹配 |
| 前端管理 UI | Vue3 + Element Plus | 纯静态 SPA,打包后由 Nginx 直接托管 |
| 外层网关 | Nginx | 路径隔离、静态文件、limit_req_zone 限流 |
| 部署方式 | Docker Compose | 共 4 个容器:nginx / sentinel-app / redis / postgres |
三、核心业务流程
3.1 请求拦截与动态绑定(网关核心逻辑)
收到请求
│
├─ 无 Authorization Header? ─→ 直接透传给 New API(由其拒绝)
│
├─ 提取 Token(sk-xxx)
├─ 提取真实 IP(只信任 Nginx 写入的 X-Real-IP,见安全说明 §6.1)
├─ 对 Token 做 HMAC-SHA256 哈希 → token_hash
│
├─ 查 Redis: sentinel:token:{token_hash}
│ │
│ 命中 ──→ 比对 IP(支持 CIDR 匹配)
│ │ ├─ 匹配 → 刷新 last_used_at(异步写 PG)→ 放行
│ │ └─ 不匹配 → 写拦截日志 → 检查告警阈值 → 返回 403
│ │
│ 未命中 → 查 PG: SELECT * FROM token_bindings WHERE token_hash=?
│ │
│ 有记录 → 回写 Redis → 比对 IP(同上)
│ │
│ 无记录 → 【首次绑定】
│ ├─ INSERT INTO token_bindings (token_hash, bound_ip, status=Active)
│ ├─ 写入 Redis,TTL 7天
│ └─ 放行请求
3.2 SSE 流式输出透传要求
- 网关必须支持 逐 chunk 实时转发,不得缓冲整个响应再转发。
- 响应 Header 中的
Content-Type: text/event-stream和Transfer-Encoding: chunked必须透传。 - 连接超时时间应设为不低于 600 秒(大模型长上下文推理可能耗时较长)。
3.3 管理员运维场景
| 场景 | 触发条件 | 管理员操作 | 系统行为 |
|---|---|---|---|
| 用户换电脑/换 IP | 用户反馈 Key 无法使用 | 在后台搜索该 Token,点击【解绑】 | 清除 PG 记录 + 清除 Redis 缓存,用户下次调用重新触发首次绑定 |
| 用户转岗,需绑定新 IP | 用户提交申请 | 在后台点击【编辑 IP】,填入新 IP 或网段 | 更新 PG + 使 Redis 缓存失效(强制下次从 PG 重新加载) |
| 发现 Key 被泄露 | 拦截告警推送 | 点击【封禁】 | 将该 Token 状态置为 Banned,Redis 同步更新,后续所有请求直接被 Sentinel 拒绝 |
| 用户离职 | HR 通知 | 点击【封禁】 | 同上 |
四、功能需求
4.1 Nginx 层职责
在 nginx.conf 中需要实现以下配置:
# 1. 代理路径:/ 全部转发给 sentinel-app:7000
# 2. 管理后台访问限制
location /admin/ {
allow 10.0.0.0/8; # 内网 IP 段
allow 192.168.0.0/16;
deny all;
proxy_pass http://sentinel-app:7000;
}
# 3. 静态文件(前端 UI)
location /admin/ui/ {
root /etc/nginx/html;
try_files $uri $uri/ /admin/ui/index.html;
}
# 4. 基础限流
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
# 5. 强制写入真实 IP(防客户端伪造)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr; # 覆盖,不信任客户端传入的值
4.2 Sentinel App 反向代理模块
- 受信 IP Header:只读取
X-Real-IP(Nginx 写入的),忽略请求中原始的X-Forwarded-For。 - Token 提取:从
Authorization: Bearer {token}Header 中提取。 - 连接池:与下游 New API 保持 HTTP Keep-Alive 长连接,连接池最大连接数可通过配置项设置,默认 512。
- 异步落库:放行后,
last_used_at更新通过内部消息通道异步批量写入 PG(每 5 秒 flush 一次),避免阻塞代理主协程。 - 降级策略:
- Redis 不可用时:降级查询 PG,同时触发内部限流(最大 QPS 降为正常值的 50%),并写入系统告警日志。
- PG 也不可用时:行为由环境变量
SENTINEL_FAILSAFE_MODE决定:open(放行所有,保业务)或closed(拒绝所有,保安全),生产环境默认closed。
4.3 Web 管理后台(Admin UI)
页面一:数据大盘(Dashboard)
- 卡片指标:
- 今日总请求数
- 今日放行数 / 拦截数
- 当前已绑定 Token 数量
- 当前封禁 Token 数量
- 折线图:最近 7 天每日放行 vs 拦截数量趋势
- 列表:最近 10 条拦截记录(实时刷新)
页面二:绑定管理(Bindings)
表格字段:
| 字段 | 说明 |
|---|---|
| ID | 数据库主键 |
| Token(脱敏) | 展示格式 sk-ab**...**xy(前 4 后 4) |
| 绑定 IP / 网段 | 支持展示 CIDR,如 192.168.1.0/24 |
| 状态 | Active / Banned |
| 首次绑定时间 | |
| 最近调用时间 | |
| 操作 | 解绑 / 编辑 IP / 封禁 / 解封 |
筛选功能:按 Token 尾号搜索、按 IP 搜索、按状态筛选。
页面三:拦截日志(Intercept Logs)
表格字段:
| 字段 | 说明 |
|---|---|
| 时间 | 拦截发生的精确时间 |
| Token(脱敏) | 被拦截的 Token |
| 绑定 IP | 该 Token 注册的合法 IP |
| 尝试 IP | 发起非法请求的 IP |
| 是否已告警 | 是否触发过告警推送 |
支持按时间范围、Token、尝试 IP 筛选,支持导出 CSV。
页面四:系统设置(Settings)
- 告警阈值配置:N 分钟内同一 Token 被拦截 M 次触发告警(默认:5 分钟内 5 次)。
- 告警方式:Webhook URL(调用方自定义,POST JSON 格式)。
- 自动归档策略:last_used_at 超过 N 天的记录自动归档(默认 90 天)。
FAILSAFE_MODE开关显示与切换。
4.4 Admin REST API
所有 /admin/api/* 接口需要携带 JWT Token(通过 /admin/api/login 获取),登录凭证从环境变量读取。
| Method | 路径 | 说明 |
|---|---|---|
POST |
/admin/api/login |
管理员登录,返回 JWT |
GET |
/admin/api/dashboard |
大盘统计数据 |
GET |
/admin/api/bindings |
获取绑定列表(支持分页 & 筛选) |
POST |
/admin/api/bindings/unbind |
解除绑定(清除 PG + Redis) |
PUT |
/admin/api/bindings/ip |
手动修改绑定 IP |
POST |
/admin/api/bindings/ban |
封禁 Token |
POST |
/admin/api/bindings/unban |
解封 Token |
GET |
/admin/api/logs |
获取拦截日志(分页 & 筛选) |
GET |
/admin/api/logs/export |
导出日志 CSV |
GET |
/admin/api/settings |
获取当前系统配置 |
PUT |
/admin/api/settings |
更新系统配置 |
GET |
/health |
健康检查(无需鉴权,供 Nginx/Docker 使用) |
五、数据结构设计
5.1 token_bindings(主表)
CREATE TABLE token_bindings (
id BIGSERIAL PRIMARY KEY,
token_hash VARCHAR(64) NOT NULL UNIQUE, -- HMAC-SHA256 哈希值
token_display VARCHAR(20) NOT NULL, -- 脱敏展示用,如 sk-ab****xy
bound_ip CIDR NOT NULL, -- 使用 PG 原生 CIDR 类型,支持网段
status SMALLINT NOT NULL DEFAULT 1, -- 1=Active, 2=Banned
first_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_token_bindings_token_hash ON token_bindings(token_hash);
CREATE INDEX idx_token_bindings_bound_ip ON token_bindings USING GIST (bound_ip inet_ops);
IP 范围匹配查询(使用 PG << 操作符,性能极高):
-- 检查请求 IP 是否在绑定的网段内
SELECT status FROM token_bindings
WHERE token_hash = $1
AND $2::inet << bound_ip -- $2 为请求方真实 IP
LIMIT 1;
5.2 intercept_logs(拦截审计日志表)
CREATE TABLE intercept_logs (
id BIGSERIAL PRIMARY KEY,
token_hash VARCHAR(64) NOT NULL,
token_display VARCHAR(20) NOT NULL,
bound_ip CIDR NOT NULL,
attempt_ip INET NOT NULL,
alerted BOOLEAN NOT NULL DEFAULT FALSE,
intercepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_intercept_logs_token_hash ON intercept_logs(token_hash);
CREATE INDEX idx_intercept_logs_intercepted_at ON intercept_logs(intercepted_at DESC);
5.3 Redis 数据结构
绑定记录缓存
- Key:
sentinel:binding:{token_hash} - Value(JSON):
{ "bound_ip": "192.168.1.0/24", "status": 1 } - TTL:604800 秒(7 天),每次命中时刷新
拦截计数器(用于告警)
- Key:
sentinel:alert:{token_hash} - Value:Integer(拦截次数计数)
- TTL:由告警配置的时间窗口决定(默认 300 秒/5 分钟)
- 当计数达到阈值时,App 触发告警并重置计数器
六、安全规范
6.1 IP 防伪造(最高优先级)
- Nginx 必须使用
proxy_set_header X-Real-IP $remote_addr;强制覆盖,$remote_addr是 TCP 层观察到的连接 IP,客户端无法伪造。 - Sentinel App 内部必须明确配置受信上游 IP 列表(即 Nginx 容器的内网 IP,如
172.18.0.2),只有来自受信上游的X-Real-IP才被采信,否则直接用 TCP 连接的原始 IP。 - 禁止在任何情况下直接信任客户端传入的
X-Forwarded-ForHeader。
6.2 Token 存储安全
- 禁止明文存储 API Key 的完整内容。
- 存储时使用 HMAC-SHA256(密钥从环境变量
SENTINEL_HMAC_SECRET读取,32 字节随机字符串)对 Token 进行哈希。 token_display字段仅存储供人识别的脱敏格式(sk-ab****xy),无法被逆推出原始 Key。
6.3 Admin 接口安全
- 管理员登录接口需要防暴力破解:连续 5 次失败则锁定 IP 15 分钟。
- JWT Token 有效期设为 8 小时,支持手动吊销(退出登录清除服务端 Session 记录)。
- 所有管理接口的调用需要记录操作日志(操作人 IP + 操作内容)。
七、环境变量配置清单
AI 开发时应将所有配置项做成环境变量,以下为完整列表:
| 环境变量名 | 必填 | 说明 | 示例值 |
|---|---|---|---|
DOWNSTREAM_URL |
✅ | 下游 New API 的地址 | http://new-api:3000 |
REDIS_ADDR |
✅ | Redis 连接地址 | redis:6379 |
REDIS_PASSWORD |
Redis 密码 | ||
PG_DSN |
✅ | PostgreSQL 连接串 | postgres://user:pass@postgres:5432/sentinel |
SENTINEL_HMAC_SECRET |
✅ | Token 哈希的 HMAC 密钥(32字节) | 随机生成 |
ADMIN_PASSWORD |
✅ | 管理员登录密码 | |
ADMIN_JWT_SECRET |
✅ | JWT 签名密钥 | 随机生成 |
TRUSTED_PROXY_IPS |
✅ | 受信上游代理 IP(Nginx 的内网 IP) | 172.18.0.2 |
SENTINEL_FAILSAFE_MODE |
✅ | 全链路故障时行为:open/closed |
closed |
APP_PORT |
App 监听端口 | 7000 |
|
ALERT_WEBHOOK_URL |
告警推送 Webhook 地址 | ||
ALERT_THRESHOLD_COUNT |
触发告警的拦截次数阈值 | 5 |
|
ALERT_THRESHOLD_SECONDS |
告警计数时间窗口(秒) | 300 |
|
ARCHIVE_DAYS |
自动归档的不活跃天数 | 90 |
八、Docker Compose 部署结构
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: sentinel-nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./frontend/dist:/etc/nginx/html/admin/ui:ro
depends_on:
- sentinel-app
networks:
- sentinel-net
sentinel-app:
image: key-ip-sentinel:latest # 本地 build
container_name: sentinel-app
build: .
restart: always
# 不暴露端口到宿主机,只在内网被 Nginx 访问
environment:
- DOWNSTREAM_URL=http://new-api:3000 # 通过 external network 访问 New API
- REDIS_ADDR=redis:6379
- PG_DSN=postgres://sentinel:password@postgres:5432/sentinel
- SENTINEL_HMAC_SECRET=${SENTINEL_HMAC_SECRET}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- TRUSTED_PROXY_IPS=172.18.0.0/16
- SENTINEL_FAILSAFE_MODE=closed
depends_on:
- redis
- postgres
networks:
- sentinel-net
- llm-shared-net # 与 New API 的共享网络(external)
redis:
image: redis:7-alpine
container_name: sentinel-redis
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- sentinel-net
postgres:
image: postgres:15
container_name: sentinel-postgres
restart: always
environment:
POSTGRES_USER: sentinel
POSTGRES_PASSWORD: ${PG_PASSWORD}
POSTGRES_DB: sentinel
volumes:
- pg_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # 初始化建表 SQL
networks:
- sentinel-net
volumes:
redis_data:
pg_data:
networks:
sentinel-net:
driver: bridge
llm-shared-net:
external: true # 与 New API 共享的已存在网络
九、非功能需求
| 指标 | 要求 |
|---|---|
| 代理延迟增加(p99) | Redis 命中路径增加延迟 ≤ 5ms |
| 并发能力 | 单实例支持 ≥ 1000 并发连接(Go 实现) |
| SSE 透传 | 流式输出无缓冲实时转发,延迟增加 ≤ 10ms |
| 可扩展性 | App 本体无状态,可横向扩展多实例,负载由 Nginx upstream 均衡 |
| 日志格式 | 结构化 JSON 日志,兼容 ELK / Loki 采集 |
| 健康检查 | GET /health 需 200ms 内响应 {"status":"ok"} |