密钥管理
Open-ACE 的加密密钥派生、轮换、环境密钥职责与安全最佳实践。
目录
概述
Open-ACE 使用 Fernet 对称加密保护静态敏感数据。同一密钥直接加密全部 8 类存储(权威完整清单见下文密钥共享影响面):
- 远程工作区的 API Key(
api_key_store表) - SMTP 密码(
smtp_settings表) - Model Gateway API Key(
model_gateway_config表) - SSO Provider 凭据(
sso_providers表) - 钉钉集成(
dingtalk_settings表) - 飞书集成(
feishu_settings表) - Webhook 配置(
webhook_settings表) - 通知偏好(
notification_preferences表)
Proxy Token 使用 HMAC-SHA256 签名(非 Fernet)进行认证。
环境密钥职责矩阵
.env.example 定义的 13 个变量分为密钥、门槛开关和普通配置三类。完整细节(默认值、出处)见 ENV_REFERENCE.md。
| 变量 | 职责 | 生产环境 | 轮转影响 |
|---|---|---|---|
SECRET_KEY | Flask 会话签名 | 必需,>= 32 字符 | 全员下线(所有已登录会话失效);无数据丢失 |
OPENACE_ENCRYPTION_KEY | 加密全部 8 类密文存储;签发 proxy token | 必需,>= 32 字符 | 不先重加密则旧密文永久无法解密(见密钥轮换);活跃 proxy token 失效 |
UPLOAD_AUTH_KEY | 工作区文件上传接口鉴权 | 建议设置 | 上传客户端需换新密钥,否则被拒;留空则禁用该接口 |
DB_PASSWORD | PostgreSQL 认证 | 必需强密码(>= 9 字符) | 需同时改 .env 和 PostgreSQL 本体并重启;不一致 = 连接失败 |
OPENACE_SECURITY_MODE | 非密钥:决定哪些密钥是强制项 | production | 切换只改变启动严格度,不影响已存数据 |
SSO_ALLOWED_REDIRECT_DOMAINS | 非密钥:SSO 重定向白名单 | 非 localhost SSO 必需 | 修改名单即改变 SSO 可跳转范围 |
DB_USER、DB_NAME | 数据库身份 | ace / ace | 非例行轮转;需重建 |
PORT、SERVER_IP、WORKSPACE_PORT_RANGE_START/END | 网络寻址 | — | 改变 URL/书签;与密钥无关 |
WORKSPACE_ISOLATION_BACKEND | 工作区隔离 backend 开关(Issue #3446;取代 WORKSPACE_MULTI_USER_MODE) | — | 改变安全形态,不涉及密钥 |
密钥派生
加密密钥从 OPENACE_ENCRYPTION_KEY 环境变量派生:
OPENACE_ENCRYPTION_KEY (环境变量,>= 32 字符)
│
│ SHA-256 哈希
▼
32 字节密钥
│
│ base64.urlsafe_b64encode
▼
Fernet 密钥 (44 字符)
│
├──────────────────────────────┐
▼ ▼
ALL 8 encrypted stores Proxy Token
(api_key_store / smtp_settings / signing (HMAC-SHA256,
model_gateway_config / not Fernet)
sso_providers / dingtalk_settings /
feishu_settings / webhook_settings /
notification_preferences —
full list in "Key Sharing Impact")
密钥派生代码:
import hashlib
import base64
key_env = os.environ.get("OPENACE_ENCRYPTION_KEY")
derived_key = hashlib.sha256(key_env.encode()).digest()
fernet_key = base64.urlsafe_b64encode(derived_key)
密钥共享影响面
同一密钥(OPENACE_ENCRYPTION_KEY,SHA-256 派生 Fernet 密钥)直接加密以下存储:
- API Key 加密 -
api_key_store.encrypted_key - SMTP 密码加密 -
smtp_settings.encrypted_password - Model Gateway 加密 -
model_gateway_config.encrypted_api_key - SSO Provider 配置 -
sso_providers(JSON 内嵌第三方凭据) - 钉钉集成 -
dingtalk_settings - 飞书集成 -
feishu_settings - Webhook 配置 -
webhook_settings - 通知偏好 -
notification_preferences
同一密钥还用于 Proxy Token 的 HMAC-SHA256 签名(远程代理认证)。
影响:
- 密钥轮换必须一次性重新加密上述全部存储——只轮换其中一部分,切换密钥后其余存储的凭据将无法解密(这正是
scripts/rotate_sso_encryption.py单事务全存储轮换的设计动机) - 使用旧密钥签名的活跃 Proxy Token 轮换后验证失败
- 密钥泄露影响上述全部安全域
密钥轮换
轮换能力与边界
- 原子全存储轮换:
scripts/rotate_sso_encryption.py在单事务内重加密该密钥保护的全部存储,任一步失败整体回滚;--verify提供无写入的 pre-flight 干跑 - 单密钥 Fernet:不支持 MultiFernet 多密钥解密(见下方"MultiFernet 支持(未来增强)")
- 切换需重启:环境变量换成新密钥后需重启服务生效
轮换方法
全存储原子轮换(rotate_sso_encryption.py)
前提条件:
- 数据库备份能力
- 计划维护窗口
- 环境变量的 root 访问权限
步骤:
-
备份数据库
# PostgreSQLpg_dump openace > openace_backup_$(date +%Y%m%d).sql# SQLitecp app.db app_backup_$(date +%Y%m%d).db -
生成新密钥(暂不启用)
NEW_KEY=$(openssl rand -hex 32) -
停止所有使用旧钥的读写者(应用、scheduler/worker 等;数据库保持运行)。这是脚本安全性的前提而非可选项:脚本不持表锁,每条 UPDATE 仅按扫描到的旧值做乐观校验(并发修改已扫描行 → 影响 0/2 行 → 整体回滚),但扫描之后新插入的旧钥行在事务内不可见,postcheck 也只能报告已提交的混钥数据而无法回滚——不停写者必然残留旧钥密文。
-
保持环境变量仍为旧密钥,先做 pre-flight 干跑
python scripts/rotate_sso_encryption.py --new-key "$NEW_KEY" --verifypre-flight 用新钥做往返探针、并用旧钥校验全部存储可解密;任何一步失败都不写入。轮换期间环境变量必须保持旧密钥——提前切成新钥会导致旧密文无法解密、pre-flight 失败。
-
执行轮换(单事务,失败整体回滚;逐条 UPDATE 行数校验,并发修改触发整体回滚)
python scripts/rotate_sso_encryption.py --new-key "$NEW_KEY"脚本重加密上述清单中该密钥直接保护的全部存储(
v1k<id>:前缀的 registry 密文绑定OPENACE_ENCRYPTION_KEYS数据钥,自动跳过),写出后用新钥复查全部存储。 -
切换环境变量到新密钥并统一重启全部服务
# Docker Compose: 编辑 .env 文件# Kubernetes: 更新 Secret# Systemd: 编辑 /etc/open-ace/environmentdocker compose restart # 或 sudo systemctl restart open-ace -
验证功能:测试 API Key、SMTP、Model Gateway、SSO 登录、钉钉/飞书/Webhook/通知;现有 Proxy Token 将失效(用户需重启会话)。
-
安全清理:验证后归档或删除数据库备份。