# 产品需求文档 (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) │ │ HTTPS (443) ▼ ┌─────────────────────────────────────────┐ │ Nginx │ │ 职责:TLS终止 / 路径路由 / │ │ 静态文件 / 内网鉴权 / 粗粒度限流 │ └────────────────┬────────────────────────┘ │ 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** | TLS 终止、路径隔离、静态文件、`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` 中需要实现以下配置: ```nginx # 1. TLS 终止(HTTPS → HTTP 转发给 sentinel-app) # 2. 代理路径:/ 全部转发给 sentinel-app:7000 # 3. 管理后台访问限制 location /admin/ { allow 10.0.0.0/8; # 内网 IP 段 allow 192.168.0.0/16; deny all; proxy_pass http://sentinel-app:7000; } # 4. 静态文件(前端 UI) location /admin/ui/ { root /etc/nginx/html; try_files $uri $uri/ /admin/ui/index.html; } # 5. 基础限流 limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m; # 6. 强制写入真实 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` | ✅ | 受信上游代理 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 部署结构 ```yaml version: '3.8' services: nginx: image: nginx:alpine container_name: sentinel-nginx ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/ssl:/etc/nginx/ssl: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"}` | ***