Files
sentinel/PRD.md

422 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 产品需求文档 (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由其拒绝
├─ 提取 Tokensk-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)
│ ├─ 写入 RedisTTL 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 状态置为 BannedRedis 同步更新,后续所有请求直接被 Sentinel 拒绝 |
| 用户离职 | HR 通知 | 点击【封禁】 | 同上 |
***
## 四、功能需求
### 4.1 Nginx 层职责
`nginx.conf` 中需要实现以下配置:
```nginx
# 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`(主表)
```sql
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 `<<` 操作符,性能极高):
```sql
-- 检查请求 IP 是否在绑定的网段内
SELECT status FROM token_bindings
WHERE token_hash = $1
AND $2::inet << bound_ip -- $2 为请求方真实 IP
LIMIT 1;
```
### 5.2 `intercept_logs`(拦截审计日志表)
```sql
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
```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-For` Header。
### 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` | ✅ | 受信上游代理 IPNginx 的内网 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 部署结构
```yaml
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"}` |
***