mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
Merge pull request #532 from fastapiadmin/dev
security(deploy): 生产凭据轮换 + 镜像不再携带口令
This commit is contained in:
+4
-3
@@ -190,7 +190,8 @@
|
||||
| Web i18n 与通知枚举 | 85/101 页面未接入 i18n;通知状态枚举与后端语义冲突 |
|
||||
| 测试覆盖与夹具 | 夹具掩盖缺陷、迁移链路 0 覆盖(CI 已建立,但用例仍需补) |
|
||||
| 移动端其它缺陷 | `alova.ts:115-119` 请求头写成 `ContentType`(缺连字符),修正需回归上传/表单/JSON 三类请求 |
|
||||
| 密钥残留 | 旧数据库口令与 OPENAI_API_KEY 仍存在于 git 历史,建议按已泄露处理并重新签发 |
|
||||
| 凭据轮换与镜像卫生 | ① 生产口令整体轮换:MySQL 应用用户 / MySQL root / Redis(旧值实测被拒:`ERROR 1045`、`WRONGPASS`);② 镜像不再携带任何口令:`deploy-artifacts.sh` 构建时剔除 `DATABASE_PASSWORD / REDIS_PASSWORD / SECRET_KEY / DATA_ENCRYPTION_KEY` 等键(审计 B3 根治),重建后镜像内口令行命中 0 条,应用改由 compose 注入口令且 `db_status/redis_status=1`;③ 本地 `backend/env/.env.prod` 中泄露值清空并改 600 权限 | 旧口令拒绝测试、新口令容器内连接测试、镜像内容扫描、四端 200、容器全 healthy |
|
||||
| 公开仓库历史暴露(需用户侧吊销) | 仓库为 **public**,历史中真实出现过:**OpenAI API Key(35 字符 `sk-…`)**、旧 MySQL/Redis/root 口令、旧 `SECRET_KEY`、旧自签名 TLS 私钥;其余 `PAYMENT_* / EMAIL_PASSWORD / DINGDING_SECRET / MONGO_DB_PASSWORD / POSTGRESQL_PASSWORD` 等仅出现在 `.example` 占位文件 | 已消除:DB/Redis/root 口令与 `SECRET_KEY`/`DATA_ENCRYPTION_KEY` 本轮轮换(历史值即失效);线上证书与历史私钥**不同**(指纹比对)。**待用户处理:到 OpenAI 控制台吊销该 Key**(第三方无法代办);是否用 `git-filter-repo` 清洗历史见 §9.2 |
|
||||
| 对外口径(异常统一后的取舍) | 存储「测试连接」密钥环损坏、上传/下载失败、OAuth 渠道未配置、Redis 同步失败等已由具体原因变为通用 5xx 文案(细节只在日志)。若希望管理员在界面看到可操作原因,正确做法是为这些场景定义**业务码**,而不是回到「包装意外异常」 |
|
||||
| 仍可能外泄的出口 | `ValueError` 全局处理器返回 `msg=str(exc)`;建议改为通用文案 + 日志,或让我们的校验统一抛 `CustomException` |
|
||||
| 静默吞异常(既有) | 48 处「记录日志但不 raise」的 best-effort 处理器(redis_crud、ap_scheduler、middlewares、discover 等),属审计「静默吞异常」主线,建议单独排一轮 |
|
||||
@@ -231,7 +232,7 @@
|
||||
|
||||
**收尾后仍需人工处理(不阻塞上线)**
|
||||
|
||||
1. 轮换 `OPENAI_API_KEY` 与旧数据库口令(已进历史/镜像层,按已泄露处理)。
|
||||
2. 审计报告内含旧密钥的少量特征(前 4 字符与长度)用于佐证;若仓库将转为公开,建议先按上述轮换再考虑是否脱敏。
|
||||
1. **吊销旧 OpenAI API Key**:仓库是 public,`sk-…`(35 字符)确实出现在 `backend/env/.env.prod` 的历史提交中。线上环境从未使用它(容器 env 与该键均为空、新镜像已不含任何密钥),所以风险只在于该 Key 在 OpenAI 侧可能仍有效。请到 <https://platform.openai.com/api-keys> 吊销;如需 AI 功能再签发新 Key 并只写入服务器 `.env`。
|
||||
2. **是否清洗 git 历史**:`git-filter-repo` 可把历史中的 `.env*` 与旧证书整体抹除,但会重写所有 SHA(需 force-push `dev` 与 `master`,影响既有克隆与 PR 引用,且 gitee 镜像需同步)。由于相关口令已轮换、Key 将吊销,**建议以「吊销 + 轮换」为准,清洗作为可选项**;若仓库要转为对外开源,则建议执行清洗。
|
||||
3. §9.2 中的产品/架构决策项(会话失效、数据权限注入、多租户规格、i18n、移动端 `ContentType`)。
|
||||
|
||||
|
||||
+34
-1
@@ -123,7 +123,40 @@ ss -tlnp | grep docker-proxy # 确认对外只有 80/443
|
||||
`pydantic_settings.exceptions.SettingsError: error parsing value for field "XXX" from source "EnvSettingsSource"`,
|
||||
就是该字段的取值格式与类型不匹配。
|
||||
|
||||
## 7. 已知遗留问题
|
||||
## 7. 凭据轮换(生产)
|
||||
|
||||
轮换对象:MySQL 应用用户 / MySQL root / Redis。三者都通过 `docker/.env` 注入,且
|
||||
**镜像内不得包含任何口令**(`deploy-artifacts.sh` 在构建上下文里剔除
|
||||
`DATABASE_PASSWORD / REDIS_PASSWORD / SECRET_KEY / DATA_ENCRYPTION_KEY` 等键,
|
||||
仅保留主机名、端口等非敏感项——审计 B3 的根治)。
|
||||
|
||||
标准步骤(约 1 分钟中断窗口,顺序不可颠倒):
|
||||
|
||||
```bash
|
||||
cd /home/FastapiAdmin/docker
|
||||
cp -a .env ".env.bak-rotate-$(date +%Y%m%d-%H%M%S)" # 先备份,便于回滚
|
||||
|
||||
# 1) 改数据库口令(用 stdin 传 SQL,避免口令出现在命令行历史/ps)
|
||||
NEW=$(openssl rand -base64 36 | tr -d '/+=' | cut -c1-30)
|
||||
docker exec -i -e MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql mysql -uroot <<SQL
|
||||
ALTER USER 'fastapiadmin'@'%' IDENTIFIED BY '$NEW';
|
||||
FLUSH PRIVILEGES;
|
||||
SQL
|
||||
|
||||
# 2) 同步写入 .env(键名:MYSQL_PASSWORD)
|
||||
# 3) 重建引用该值的容器:mysql → redis → backend
|
||||
# 注意:mysql/redis 的健康检查命令内插了 .env 值,必须一并重建,否则健康检查会一直失败
|
||||
docker compose up -d --force-recreate mysql && sleep 20
|
||||
docker compose up -d --force-recreate redis
|
||||
docker compose up -d --force-recreate backend
|
||||
|
||||
# 4) 验证:旧口令必须被拒(1045 / WRONGPASS),新口令可用,健康检查 db_status/redis_status = 1
|
||||
```
|
||||
|
||||
排查提示:`docker exec -i` 会让容器内进程读走脚本自身的 stdin(若脚本经 `bash -s` 传入,
|
||||
后续步骤会被吞掉)——务必加 `</dev/null` 或改用 `-e` 直传。
|
||||
|
||||
## 8. 已知遗留问题
|
||||
|
||||
安全与质量债的完整清单见 `AUDIT-REPORT.md` 与各分报告(`backend/audit-*.md`、`frontend/audit-*.md`、`docker/audit-deploy.md`)。当前仍待处理的高优先级项:
|
||||
|
||||
|
||||
+11
-2
@@ -146,8 +146,9 @@ deploy_backend() {
|
||||
rm -rf /tmp/fa-ctx/requirements && mkdir -p /tmp/fa-ctx/requirements
|
||||
cp -R backend/requirements/. /tmp/fa-ctx/requirements/
|
||||
|
||||
# 镜像内的 .env.prod 只是兜底:数据库/Redis 真实地址由 compose 运行时注入。
|
||||
# 这里抹掉本地私网地址与所有密钥,避免把开发凭据烤进镜像。
|
||||
# 镜像内的 .env.prod 只是兜底:数据库/Redis 真实地址与口令由 compose 运行时注入。
|
||||
# 这里抹掉本地私网地址与所有密钥/口令,避免把任何凭据烤进镜像层
|
||||
# (审计 B3:旧镜像里曾烘进真实 DATABASE_PASSWORD / REDIS_PASSWORD)。
|
||||
python3 - "$db" <<'PY'
|
||||
import re, pathlib, sys
|
||||
db = sys.argv[1]
|
||||
@@ -156,6 +157,12 @@ force = {"DATABASE_HOST": "localhost", "REDIS_HOST": "localhost"}
|
||||
if db == "postgres":
|
||||
force.update({"DATABASE_TYPE": "postgres", "DATABASE_PORT": "5432", "DATABASE_USER": "postgres"})
|
||||
drop_prefixes = ("OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_MODEL")
|
||||
# 口令类:运行时全部由 compose 注入,镜像内保留会成为泄露面
|
||||
drop_keys = {
|
||||
"DATABASE_PASSWORD", "REDIS_PASSWORD",
|
||||
"SECRET_KEY", "DATA_ENCRYPTION_KEY", "DATA_ENCRYPTION_OLD_KEYS",
|
||||
"MYSQL_PASSWORD", "MYSQL_ROOT_PASSWORD",
|
||||
}
|
||||
out = []
|
||||
for line in p.read_text().splitlines():
|
||||
m = re.match(r"^(\s*)([A-Z_]+)(\s*=\s*)(.*)$", line)
|
||||
@@ -163,6 +170,8 @@ for line in p.read_text().splitlines():
|
||||
out.append(f"{m.group(1)}{m.group(2)}{m.group(3)}{force[m.group(2)]}")
|
||||
elif m and m.group(2).startswith(drop_prefixes):
|
||||
continue
|
||||
elif m and m.group(2) in drop_keys:
|
||||
continue
|
||||
else:
|
||||
out.append(line)
|
||||
p.write_text("\n".join(out) + "\n")
|
||||
|
||||
Reference in New Issue
Block a user