Repository navigation
feat(security): make session encryption a first-class deployment control - #28
Merged
Merged
Conversation
会话数据加密此前只有**代码侧**能力(envelope + fail-closed),生产配置侧几乎
是空白:`SESSION_ENCRYPTION_KEY` 不在任何 compose 环境块里,`SESSION_ENCRYPTION_REQUIRED`
不存在,密钥轮换没有受支持的路径,`SessionEncryptionError` 没有 API 契约,
"还剩多少历史明文"只能靠猜。
本 PR 补齐**配置与可观测**层,全部改动**非破坏性**:不删除、不迁移、不改写任何
既有会话记录,不轮换任何既有密钥,不切换任何真实部署的 reject 模式。
## 1. 部署契约(SESSION_ENCRYPTION_KEY 进入生产配置)
compose 的 app / worker / canary / scale / prod 现在都注入
`SESSION_ENCRYPTION_KEY` / `SESSION_ENCRYPTION_REQUIRED` /
`SESSION_ENCRYPTION_PLAINTEXT_COMPAT` / `SESSION_ENCRYPTION_KEY_PREVIOUS`。
`.env.prod` 模板带占位符且 `SESSION_ENCRYPTION_REQUIRED=true`;
`generate_prod_env.py` 会生成 32 字节随机密钥。
app 与 worker 必须用**同一把**密钥(worker 也读写会话记录,且与 app 共用同一
会话存储);canary / scale 同理。
compose 层刻意**不用** `${SESSION_ENCRYPTION_KEY:?}`:那会让 `docker compose
config` 在配置阶段失败,既有生产环境连诊断都进不去。策略放在**应用启动校验**
里,错误信息指名变量、可操作。
## 2. 生产模式的明确安全策略
新增 `SESSION_ENCRYPTION_REQUIRED`(默认 false = 保持既有部署行为):
置 true 后,生产缺少密钥、或密钥仍是占位符/弱值,一律**拒绝启动**(fail closed),
而不是像此前那样只打一条 warning 就继续以明文运行。
为什么是开关而不是无条件强制:无条件强制会让「刚升级 compose、还没配密钥」的
既有生产环境直接起不来。开关把收紧变成一次显式、可审计的部署决策。
**收紧不破坏既有真实数据**:legacy 模式下历史明文仍可读,被改写的会话自动升级
为密文。「配密钥」与「迁移历史明文」是两件独立的事。
## 3. 无破坏密钥轮换
新增 `SESSION_ENCRYPTION_KEY_PREVIOUS`(逗号分隔的历史密钥)。没有 key-id 时,
直接换密钥 = 一次性作废全部既有密文。现在:旧密钥进 previous,旧密文仍可解密,
新写入用当前密钥,旧会话在被改写时自动升级为密文。
`scripts/rotate_secrets.py` 已接入该密钥,并在轮换时**自动**把旧值写入 previous
(而不是直接替换)。`session_encryption_key_fallback_total` 计数让运维能判断
轮换是否排空。
## 4. 错误契约与可观测性
- `SessionEncryptionError` 增加 API 异常处理器:HTTP 500 + 稳定错误码
`SESSION_ENCRYPTION_ERROR`。响应体**只含**错误码,不含密钥、密文、记录内容、
session_id。
- 新增三个指标(label 只能是 kind/reason,绝不携带会话标识):
`session_encryption_error_total{reason}`、`session_legacy_plaintext_read_total{kind}`、
`session_encryption_key_fallback_total`。
- `session_legacy_plaintext_read_total` 是**迁移前的盘点信号**:切 reject 之前
必须先知道历史明文还有多少,现在不再只能靠猜。
## 5. 存储访问控制(部署要求)
`scripts/pre_deploy_check.sh` 新增:强制加密下密钥缺失/占位符 -> 阻断;
Redis 默认密码 `changeme` -> 告警;`REDIS_URL` 未用 `rediss://` -> 告警;
明文兼容策略取值非法 -> 阻断。
**没有**改造 Redis 容器(加 ACL / 拆凭据会破坏既有部署),因此存储侧访问控制
仍是 **NOT_VERIFIED**。
## 边界(不夸大)
- 会话加密保护的是**记录内容**,**不是会话存储**。拥有 Redis / 会话文件写权限的
攻击者仍能替换记录 —— 真正的控制是存储侧访问控制(Issue #25)。
- 本 PR 全部测试使用**合成数据**(tmp_path / 内存替身),验证的是**治理机制**,
**不是**真实生产会话迁移的验证。
- 仍**没有**明文->密文的批量回填脚本、dry-run 盘点器、备份/回滚步骤。
- **Issue #25 保持 OPEN**:关闭条件(真实数据 artifact + 来源可验证性结论 +
回填完成 + reject 启用证据)一项都未满足。
- 未对生产 Redis 做任何操作、未执行真实会话迁移、未轮换既有密钥、未切换真实
部署的 reject 模式。
## 验证(基线 SHA a362fe1)
| 项 | 结果 |
|---|---|
| `pytest tests/unit/` | **3545 passed, 5 skipped**(3550 collected,新增 24 条) |
| `pytest tests/integration/runtime/`(真实 PG 15.18 + Redis 7.4.9 + Celery 5.6.3) | **199 passed** |
| `pytest tests/unit/test_session_encryption_deployment.py` | 24 passed(新增) |
| `scripts/audit_doc_consistency.py` | OK(57 active documents) |
| `scripts/check_public_repository_boundary.py` | OK |
| `ruff check .` + `ruff format --check .`(canonical 0.4.0) | All checks passed / 413 files formatted |
| `make openapi-check` | OK(63 paths) |
| `docker compose config`(base / +prod / +canary / +scale) | 全部 OK,四个变量正确落到 app 与 worker |
Xander-Xai
deleted the
security/session-encryption-deployment-controls
branch
October 21, 2026 07:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
会话数据加密此前只有代码侧能力(envelope + fail-closed),生产配置侧几乎是空白。本 PR 补齐配置与可观测层,全部改动非破坏性:不删除、不迁移、不改写任何既有会话记录,不轮换任何既有密钥,不切换任何真实部署的
reject模式。1. 部署契约 ——
SESSION_ENCRYPTION_KEY进入生产配置compose 的
app/worker/canary/scale/prod现在都注入SESSION_ENCRYPTION_KEY/SESSION_ENCRYPTION_REQUIRED/SESSION_ENCRYPTION_PLAINTEXT_COMPAT/SESSION_ENCRYPTION_KEY_PREVIOUS。.env.prod模板带占位符且SESSION_ENCRYPTION_REQUIRED=true;generate_prod_env.py生成 32 字节随机密钥。app 与 worker 必须用同一把密钥(worker 也读写会话记录,且与 app 共用同一会话存储);canary / scale 同理。
compose 层刻意不用
${SESSION_ENCRYPTION_KEY:?}—— 那会让docker compose config在配置阶段失败,既有生产环境连诊断都进不去。策略放在应用启动校验里,错误信息指名变量、可操作。2. 生产模式的明确安全策略
新增
SESSION_ENCRYPTION_REQUIRED(默认false= 保持既有部署行为):置true后,生产缺少密钥、或密钥仍是占位符/弱值,一律拒绝启动(fail closed),而不是像此前那样只打一条 warning 就继续以明文运行。收紧不破坏既有真实数据:
legacy模式下历史明文仍可读,被改写的会话自动升级为密文。「配密钥」与「迁移历史明文」是两件独立的事。3. 无破坏密钥轮换
新增
SESSION_ENCRYPTION_KEY_PREVIOUS(逗号分隔历史密钥)。没有 key-id 时直接换密钥 = 一次性作废全部既有密文。现在:旧密钥进 previous,旧密文仍可解密,新写入用当前密钥,旧会话在被改写时自动升级为密文。scripts/rotate_secrets.py已接入该密钥,并在轮换时自动把旧值写入 previous。session_encryption_key_fallback_total让运维能判断轮换是否排空。4. 错误契约与可观测性
SessionEncryptionError增加 API 异常处理器:HTTP 500 + 稳定错误码SESSION_ENCRYPTION_ERROR。响应体只含错误码,不含密钥、密文、记录内容、session_id。session_encryption_error_total{reason}、session_legacy_plaintext_read_total{kind}、session_encryption_key_fallback_total。session_legacy_plaintext_read_total是迁移前的盘点信号:切reject之前必须先知道历史明文还有多少,现在不再只能靠猜。5. 存储访问控制(部署要求)
scripts/pre_deploy_check.sh新增:强制加密下密钥缺失/占位符 → 阻断;Redis 默认密码changeme→ 告警;REDIS_URL未用rediss://→ 告警;明文兼容策略取值非法 → 阻断。没有改造 Redis 容器(加 ACL / 拆凭据会破坏既有部署),因此存储侧访问控制仍是 NOT_VERIFIED。
边界(不夸大)
reject模式。验证(基线 SHA
a362fe1)pytest tests/unit/pytest tests/integration/runtime/(真实 PG 15.18 + Redis 7.4.9 + Celery 5.6.3)pytest tests/unit/test_session_encryption_deployment.pyscripts/audit_doc_consistency.pyscripts/check_public_repository_boundary.pyruff check .+ruff format --check .(canonical 0.4.0)make openapi-checkdocker compose config(base / +prod / +canary / +scale)安全验收对照
TestProductionEncryptionPolicy/TestReadPathFailureModes::test_fernet_unavailable_never_falls_back_to_plaintextTestProductionEncryptionPolicy::test_required_without_key_is_an_error/test_required_with_placeholder_key_is_an_errorTestReadPathFailureModes(wrong key / corrupt / legacy plaintext / reject)TestNoSecretLeak(错误信息 + API 响应体)TestStorageWriteAccessIsNotDefended(钉住不可区分性)docs/limitations.md§9 均如实标注