ThinkWatch 安全文档#
本文档描述了 ThinkWatch 的安全架构、机制和加固实践。
1. 认证#
ThinkWatch 支持三种认证机制。网关接受其中任意一种;控制台管理 API 需要 JWT。
1.1 JWT 令牌#
ThinkWatch 在认证成功后签发两个 JWT 令牌:
| 令牌 | 默认有效期 | 用途 |
|---|---|---|
| 访问令牌 | 900 秒(15 分钟) | 用于 API 授权的短期凭证 |
| 刷新令牌 | 604800 秒(7 天) | 用于获取新的访问令牌/刷新令牌对 |
两个 TTL 值均可通过管理员 Web UI(jwt_access_ttl_seconds 和 jwt_refresh_ttl_seconds 设置)进行配置,无需重启服务器。
签名算法: 使用共享密钥(JWT_SECRET 环境变量)的 HS256。计划在未来版本中迁移到使用非对称密钥对的 RS256。
密钥要求:
JWT_SECRET必须至少 32 个字符长。- 启动时,ThinkWatch 执行熵检查,拒绝明显弱的密钥(例如全部相同字符、常见模式)。
- 使用以下命令生成强密钥:
openssl rand -hex 32
时钟偏差容忍: 在验证令牌过期(exp)和生效时间(nbf)声明时,应用 30 秒的宽限,以适应分布式服务之间的轻微时钟差异。
令牌声明:
{
"sub": "user-uuid",
"email": "user@example.com",
"role": "admin",
"exp": 1711930500,
"iat": 1711929600,
"iss": "thinkwatch"
}
刷新流程: 客户端将刷新令牌发送到 POST /api/auth/refresh。服务器验证令牌,签发新的访问/刷新令牌对,并使旧的刷新令牌失效。系统会检测刷新令牌的重用,一旦发现将使该用户的所有令牌失效(带重放检测的轮换机制)。
1.2 API 密钥#
API 密钥为网关的编程访问提供长期认证。
| 属性 | 详情 |
|---|---|
| 格式 | 以 tw- 为前缀(如 tw-sk-a1b2c3d4...) |
| 存储 | SHA-256 哈希值存储在 PostgreSQL 中 |
| 验证 | 中间件对传入的密钥进行哈希后查找(过滤 deleted_at IS NULL) |
| 范围限制 | 可选的 allowed_models 限制 |
| 速率限制 | 可选的按密钥 RPM 限制,通过 Redis 实施 |
| 过期 | 创建时可设置可选的 TTL |
原始密钥值仅在创建时返回一次。之后无法再检索,因为仅存储了哈希值。
生命周期管理:
API 密钥现在支持完整的生命周期管理,包含以下能力:
- 轮换: 可通过
POST /api/keys/{id}/rotate轮换密钥。生成新密钥并返回;旧密钥进入可配置的宽限期,在此期间新旧密钥均被接受。grace_period_ends_at时间戳指示旧密钥何时停止工作。 - 宽限期: 轮换后,API 密钥认证中间件在
grace_period_ends_at到期前同时接受新旧密钥哈希,允许客户端无中断地完成过渡。 - 不活跃超时: 密钥可配置
inactivity_timeout_days值。如果密钥在该期间内未被使用,后台生命周期任务将自动禁用该密钥。 - 自动禁用: 超过不活跃超时的密钥会被软禁用(非删除),允许管理员在需要时重新启用。
- 过期监控: 使用
GET /api/keys/expiring?days=N列出即将过期的密钥。 - 团队验证: API 密钥创建时会验证团队成员身份 — 用户只能在其所属团队内创建密钥。
1.3 OIDC / SSO#
ThinkWatch 支持 OpenID Connect,用于与企业身份提供商(Entra ID、Okta、Keycloak、Auth0 等)进行单点登录。
流程:授权码模式
- 用户浏览器被导向
GET /api/auth/sso/authorize。 - 服务器生成加密随机的
state和nonce,将两者存储在 Redis 中(TTL 为 10 分钟),然后将浏览器重定向到 OIDC 提供商的授权端点。 - 认证后,身份提供商重定向回
GET /api/auth/sso/callback?code=...&state=...。 - 服务器验证
state与 Redis 中的值(CSRF 防护),交换授权码获取令牌,验证 ID 令牌签名和nonce,并配置或更新本地用户记录。 - ThinkWatch JWT 令牌返回给客户端。
CSRF 防护: state 参数是一个一次性随机值,存储在 Redis 中。在回调时被消费(删除),防止重放攻击。
1.4 网关双重认证#
网关中间件按以下顺序尝试认证:
- 如果
Authorization请求头包含以tw-开头的令牌,则将其作为 API 密钥验证。 - 否则,将其作为 JWT Bearer 令牌验证。
这允许编程客户端(API 密钥)和基于浏览器/控制台的用户(JWT)无缝访问网关。
2. 授权(RBAC)#
ThinkWatch 实现了基于角色的访问控制,包含五个系统角色。
2.1 角色层级#
| 角色 | 描述 |
|---|---|
admin | 完全系统访问权限。可以管理提供商、用户、MCP 服务器、设置,以及查看审计日志。 |
operator | 可以管理提供商和 MCP 服务器。不能管理用户或系统设置。 |
user | 标准用户。可以创建/管理自己的 API 密钥、查看分析数据和使用网关。 |
viewer | 只读访问。可以查看分析数据和模型,但不能创建密钥或修改任何内容。 |
service | 机器对机器身份。仅通过 API 密钥访问网关;无控制台访问权限。 |
2.2 访问控制矩阵#
| 资源 | admin | operator | user | viewer | service |
|---|---|---|---|---|---|
| Gateway(聊天、模型) | 是 | 是 | 是 | 是 | 是 |
| Gateway(MCP) | 是 | 是 | 是 | 是 | 是 |
| 自有 API 密钥(CRUD) | 是 | 是 | 是 | 否 | 否 |
| 分析(只读) | 是 | 是 | 是 | 是 | 否 |
| 提供商(CRUD) | 是 | 是 | 否 | 否 | 否 |
| MCP 服务器(CRUD) | 是 | 是 | 否 | 否 | 否 |
| 用户(CRUD) | 是 | 否 | 否 | 否 | 否 |
| 审计日志 | 是 | 否 | 否 | 否 | 否 |
| 系统设置 | 是 | 否 | 否 | 否 | 否 |
2.3 中间件强制执行#
require_auth—— 从Authorization请求头中提取并验证 JWT。对未认证的请求返回 401 拒绝。require_admin—— 在require_auth之后链式执行。检查 JWT 的role声明是否为admin。对非管理员用户返回 403 拒绝。- API 密钥中间件 —— 网关专用。验证 API 密钥哈希,检查过期状态,强制执行
allowed_models,并通过 Redis 应用速率限制。
2.4 MCP 工具级访问控制#
MCP 工具可以在 API 密钥或用户级别进行限制。当 API 密钥设置了 allowed_models 时,MCP 工具调用也会受到管控:网关在将请求代理到上游 MCP 服务器之前,会检查调用方身份是否有权调用该特定工具。
3. 加密#
3.1 静态加密#
提供商 API 密钥
提供商 API 密钥(上游 AI 服务如 OpenAI、Anthropic、Google、Azure OpenAI 和 AWS Bedrock 的凭证)在存储前进行加密:
- 算法:AES-256-GCM
- Nonce:12 字节,密码学随机生成,每次加密操作独立生成
- 密钥:从
ENCRYPTION_KEY环境变量派生(32 字节十六进制字符串) - 存储格式:
nonce || ciphertext || tag(base64 编码)
AWS Bedrock 凭证
AWS Bedrock 凭证(以 ACCESS_KEY_ID:SECRET_ACCESS_KEY 格式存储)使用相同的 AES-256-GCM 方案进行静态加密。在请求时,凭证会被解密并通过官方 aws-sigv4 Rust crate 用于 AWS SigV4 请求签名。签名过程在内存中执行,凭证不会以明文形式写入磁盘。
MCP 服务器认证密钥
上游 MCP 服务器的认证凭证使用与提供商 API 密钥相同的 AES-256-GCM 方案。
3.2 密码哈希#
用户密码使用 Argon2id 进行哈希,参数如下:
| 参数 | 值 |
|---|---|
| 算法 | Argon2id |
| 内存开销 | 19 MiB |
| 时间开销 | 2 次迭代 |
| 并行度 | 1 |
| 盐值 | 16 字节,随机 |
| 输出哈希 | 32 字节 |
Argon2id 是 OWASP 指南推荐的算法,同时提供对 GPU 攻击和侧信道攻击的抵抗能力。
密码复杂度#
所有密码设置操作(注册、初始化向导、密码修改、管理员创建用户)均强制要求:
- 最少 8 个字符
- 至少一个大写字母 (A-Z)
- 至少一个小写字母 (a-z)
- 至少一个数字 (0-9)
3.3 API 密钥哈希#
API 密钥在存储前使用 SHA-256 进行哈希。这是单向操作;无法从哈希值推导出原始密钥。原始密钥仅在创建时返回给用户一次。
3.4 传输加密#
ThinkWatch 本身不终止 TLS。TLS 终止应由反向代理(Nginx、Caddy、Traefik、云负载均衡器)处理。详见第 6 节的加固清单。
4. 网络安全#
4.1 双端口架构#
ThinkWatch 将关注点分离到两个端口:
| 端口 | 服务器 | 暴露范围 |
|---|---|---|
| 3000 | Gateway | 用于应用/客户端访问 |
| 3001 | Console | 用于管理员/内部访问 |
这种分离允许网络级隔离:网关端口可以暴露给应用流量,而控制台端口限制在 VPN 或内部网络中。
4.2 CORS#
跨域资源共享通过 CORS_ORIGINS 环境变量配置:
- 接受逗号分隔的允许来源列表。
- 允许凭证(
Access-Control-Allow-Credentials: true)。 - 仅将指定来源反映在
Access-Control-Allow-Origin中。 - 在开发环境中,通常允许
http://localhost:5173(Vite 开发服务器)。 - 在生产环境中,限制为实际的控制台域名。
会话 IP 绑定#
用户登录时,签名密钥与客户端 IP 地址绑定。后续请求中,签名验证中间件会检查请求 IP 是否与登录时存储的 IP 匹配。如果 IP 不同,请求将被拒绝(401 Unauthorized)。
此机制防止被窃取的签名密钥在不同网络中被使用。
4.3 安全响应头#
所有响应中设置以下请求头:
| 请求头 | 值 | 用途 |
|---|---|---|
X-Content-Type-Options | nosniff | 防止 MIME 类型嗅探 |
X-Frame-Options | DENY | 通过 iframe 防止点击劫持 |
内容安全策略(仅控制台端口):
控制台端口(3001)包含 Content-Security-Policy 响应头,以缓解 XSS 和数据注入攻击。该策略将脚本来源、样式来源和连接端点限制为已知来源。
4.4 请求超时#
| 服务器 | 超时时间 | 原因 |
|---|---|---|
| Gateway | 120 秒 | LLM 补全(尤其是流式传输)可能较慢 |
| Console | 30 秒 | 管理操作应快速完成 |
4.5 容器安全#
生产 Docker 镜像使用 distroless 基础镜像:
- 无 shell(
/bin/sh、/bin/bash) - 无包管理器
- 无不必要的系统工具
- 最小攻击面:仅包含编译后的 Rust 二进制文件及其运行时依赖
5. 审计跟踪#
5.1 记录内容#
每个与安全相关的操作都会生成审计条目:
| 类别 | 记录的操作 |
|---|---|
| 认证 | 登录成功、登录失败、注册、令牌刷新 |
| API 密钥 | 创建、撤销、使用(速率限制命中) |
| 提供商 | 创建、更新、删除 |
| MCP 服务器 | 创建、删除、工具发现 |
| 用户 | 创建、角色变更、删除 |
| 设置 | 任何配置变更 |
5.2 审计条目 Schema#
每条审计日志条目包含:
{
"id": "uuid",
"timestamp": "2026-03-28T09:15:00.000Z",
"user_id": "uuid",
"user_email": "admin@example.com",
"action": "provider.create",
"resource_type": "provider",
"resource_id": "uuid",
"details": {},
"ip_address": "10.0.1.50",
"user_agent": "Mozilla/5.0..."
}
5.3 ClickHouse 集成#
审计条目被插入到 ClickHouse 中,用于 SQL 查询和分析:
- 数据库:可通过
CLICKHOUSE_DB配置(默认:think_watch) - 条目异步发送,以避免阻塞请求处理。
- 控制台在
/api/audit/logs提供搜索 UI,支持时间范围过滤和 SQL 查询。
5.4 日志转发 / SIEM 集成#
对于企业环境,ThinkWatch 可以通过管理员 Web UI(管理 > 日志转发器)将审计日志转发到外部系统:
- 支持的传输方式: UDP Syslog、TCP Syslog (RFC 5424)、Kafka、HTTP Webhook
- 配置: 通过数据库动态管理——无需重启
- 格式: Syslog 传输使用 RFC 5424 结构化数据:
<14>1 2026-03-28T09:15:00.000Z thinkwatch - - - [thinkwatch@0 action="provider.create" user="admin@example.com" resource_type="provider" resource_id="uuid"] Provider created: openai-prod
这允许与 SIEM 平台集成,如 Splunk、Elastic SIEM、Microsoft Sentinel 等。
6. 启动验证#
ThinkWatch 在启动服务器前验证所有密钥和依赖。如果任何关键要求未满足,进程将以清晰的错误消息退出,而不是在降级状态下运行。
启动时验证的内容:
JWT_SECRET存在、至少 32 个字符,且通过熵检查ENCRYPTION_KEY存在且为有效的 64 字符十六进制字符串- PostgreSQL 可达并响应测试查询
- Redis 可达并响应 PING
- OIDC 变量全部设置或全部未设置(不接受部分配置)
- ClickHouse 连通性(如已配置;记录警告但不阻止启动)
7. 初始化端点安全#
POST /api/setup/initialize 端点允许在无需认证的情况下创建首个管理员用户。为防止滥用:
- 速率限制: 该端点限制为每个 IP 地址每分钟 5 次请求。
- 二次检查: 在创建管理员用户前,端点会执行数据库查询以验证没有管理员用户存在。这防止了两个并发请求都创建管理员账户的竞态条件。
- 使用后禁用: 系统初始化后,该端点对所有后续调用返回
400 Bad Request,无论速率限制状态如何。
8. 软删除与数据保留#
ThinkWatch 对关键资源使用软删除:
| 资源 | 行为 |
|---|---|
| 用户 | 设置 deleted_at;撤销所有会话;用户无法登录 |
| 提供商 | 设置 deleted_at;提供商的模型对新请求不可用 |
| API 密钥 | 设置 deleted_at;认证中间件立即拒绝该密钥 |
数据保留策略:
- 软删除的记录在可配置的期间内保留(默认:30 天,由
data_retention_days设置控制)。 - 后台任务定期清除超过保留期的记录。
- 通过 API 进行的账户删除始终是软删除操作:用户的会话被撤销,并标记
deleted_at。记录仅在保留期到期后才被永久移除。
9. 加固清单#
在准备 ThinkWatch 的生产部署时,请使用此清单。
密钥与密码学#
- 将
JWT_SECRET设置为密码学随机值(最少 32 个字符,建议 64 个十六进制字符 / 256 位):openssl rand -hex 32 - 将
ENCRYPTION_KEY设置为随机的 32 字节十六进制字符串:openssl rand -hex 32 - 定期轮换
JWT_SECRET(注意:轮换会使所有活跃令牌失效) - 将所有密钥存储在专门的密钥管理器中(Vault、AWS Secrets Manager、K8s Secrets),而非明文
.env文件 - 验证启动验证无警告通过(检查日志中的熵检查结果)
网络#
- 将
CORS_ORIGINS设置为实际的控制台域名(如https://console.example.com) - 将控制台(端口 3001)部署在 VPN 或企业防火墙后面;不要暴露给公共互联网
- 在反向代理(Nginx、Caddy、Traefik 或云负载均衡器)上启用 TLS 终止
- 在反向代理上配置 HSTS 响应头
- 如果可能,将网关(端口 3000)访问限制为已知的 CIDR 范围
认证#
- 配置 OIDC 以与企业身份提供商进行 SSO
- 在生产环境中禁用基于密码的注册(使用管理员配置的账户或 SSO)
- 如果启用了密码认证,强制执行强密码策略
- 在登录端点上设置速率限制以缓解暴力攻击
数据库与基础设施#
- 配置 PostgreSQL 要求 TLS(在
DATABASE_URL中使用sslmode=require) - 启用 Redis 认证(
requirepass指令) - 如果可用,使用 Redis TLS(
rediss://协议) - 将 PostgreSQL 访问限制为仅 ThinkWatch 服务账户
- 仅从特权 CI/CD 流水线运行数据库迁移,而非在运行时从应用程序执行
API 密钥生命周期#
- 为 API 密钥设置适当的
inactivity_timeout_days以自动禁用未使用的密钥 - 建立密钥轮换计划,使用
POST /api/keys/{id}/rotate实现零停机轮换 - 通过
GET /api/keys/expiring?days=30定期审查即将过期的密钥 - 配置
data_retention_days以符合数据保留策略
审计与监控#
- 为应用日志设置日志轮转
- 验证 ClickHouse 审计表正在被填充
- 如适用,通过管理 > 日志转发器配置日志转发到您的 SIEM
- 为
auth.login_failed激增设置告警(可能的暴力攻击) - 使用基础设施监控系统监控
/api/health、/health/live和/health/ready端点 - 配置 Prometheus 抓取网关端口(3000)的
/metrics端点 - 为 API 密钥不活跃和过期事件设置告警
容器与运行时#
- 使用 distroless 生产镜像(无 shell、无包管理器)
- 以非 root 用户运行容器
- 尽可能设置只读文件系统
- 应用资源限制(CPU、内存)以防止失控进程
- 在 CI/CD 中扫描容器镜像的 CVE
RBAC#
- 审查所有用户角色,确保最小权限分配
- 定期审计管理员账户
- 对机器对机器的集成使用
service角色 - 在 API 密钥上限制
allowed_models,仅包含每个使用者需要的模型