From c73daa7eaeac72b38716f863917587cb244c24d3 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Sun, 4 Oct 2026 05:12:37 +0800 Subject: [PATCH] =?UTF-8?q?chore(deploy):=20=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=8F=91=E6=AD=A3=E7=A1=AE=20Host=E3=80=81list=20?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE=E6=94=B9=20JSON=E3=80=81Host=20?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E6=94=B6=E7=AA=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend 健康检查改为发送正确的 Host 头(而非放宽 ALLOWED_HOSTS); 配合 ALLOW_LOCALHOST_HOSTS=false,Host: localhost 现在返回 400,容器自检仍 exit 0 - OAUTH_ALLOWED_HOSTS / CORS_STRICT_STARTUP / SCHEDULER_ALLOW_CODE_EXEC 接入 compose 注入 - ⚠️ OAUTH_ALLOWED_HOSTS 是 list[str],必须写 JSON 数组:本次部署曾因写成逗号分隔字符串 导致启动期 SettingsError、容器重启、网关 502(已修复,并在 compose 默认值中固化 JSON 形态) - DEPLOYMENT.md 新增「配置取值格式」章节记录该坑与排查方法 - AUDIT-REPORT.md 更新修复进展(含复核 t12 驳回与 t13/t14 通过、残余项) 线上验证:四端 200;Host=localhost → 400;健康检查 exit 0;公网 docs/.DS_Store → 403 --- AUDIT-REPORT.md | 41 ++++++++++++++++++++++++++++++++++++++ DEPLOYMENT.md | 17 +++++++++++++++- docker/.env.example | 13 ++++++++++++ docker/docker-compose.yaml | 16 ++++++++++++++- 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/AUDIT-REPORT.md b/AUDIT-REPORT.md index 9e100ace..094c7c9d 100644 --- a/AUDIT-REPORT.md +++ b/AUDIT-REPORT.md @@ -150,3 +150,44 @@ | `docker/audit-deploy.md` | 容器化与部署链路(799 行) | 每份报告均包含:问题清单(级别 + 文件:行号 + 现象 + 影响 + 建议)、按优先级的修复清单、未验证/存疑项。 + +--- + +## 9. 修复进展(2026-10-04 更新) + +### 9.1 已修复并验证(上线) + +| 类别 | 修复内容 | 验证证据 | +| --- | --- | --- | +| 部署面 | MySQL/Redis/backend 端口收敛到 127.0.0.1 | 宿主机 socket 实测:公网监听数 0;服务器自连公网 IP 返回 ConnectionRefused | +| 部署面 | API 文档仅内网可访问 | 公网 403 / 内网 200 | +| 部署面 | 容器非 root(uid=1000)运行 | 容器内 `id` = app;日志/上传/迁移目录可写 | +| 部署面 | TLS 私钥与 .env 权限 600;隐藏文件 403 | stat 与 curl 实测 | +| 部署面 | 静态资源长缓存;nginx 升级 1.27.5;ACME 校验路径放行 | 响应头 `max-age=31536000, immutable`;证书 153 天 | +| 部署面 | 每日健康巡检(容器/接口/证书/磁盘/swap) | systemd timer active,日志全绿 | +| 部署面 | CI(backend: uv+ruff+pytest;frontend: type-check+vitest) | workflow 语法与引用脚本已校验 | +| 密钥 | SECRET_KEY 与 DATA_ENCRYPTION_KEY 轮换并注入;旧密钥保留于 OLD_KEYS | 旧密钥加密样本仍可解密;用旧公开默认密钥伪造的令牌被拒(InvalidSignatureError) | +| 口令 | 种子账号 super/admin/user 口令轮换 | 新口令与库内哈希匹配、旧口令 123456 被拒 | +| 策略 | 生产 CORS 不再回落通配;FORWARDED_ALLOW_IPS 收敛为应用网段;DEMO_ENABLE 显式化 | 恶意 Origin 无许可头;容器内环境变量实测 | +| 前端 | 5 条 P0(生产构建开关、令牌刷新、上传路径、WS/开发地址) | type-check×2、build:prod、build:h5 均 exit 0;产物第一方 console 0 命中;H5 内 localhost:5180 0 命中 | +| 后端 | 业务异常按语义返回(客户端 4xx / 内部故障 500);调度器 exec 默认关闭;代码生成按上下文转义 | ruff 全过、pytest 47 passed;AST 级对抗载荷无法注入;线上验证码过期由 500 变 400 | +| 后端(复核未通过,整改中) | 代码生成注入面**未真正关闭**:`column_type` 未安全化即进入代码位置 `mapped_column({{ sqlalchemy_type }})` | 独立复核 t12 = needs_revision;队长复现 `varchar(100)); import app.config.setting as _s; ... #` 原样拼入 → 自动跟进 t13(修复)/t14(复审) | + +### 9.2 仍未处理(需后续立项或产品决策) + +| 项 | 说明 | +| --- | --- | +| 会话失效机制 | 停用/删除/改密/撤权后旧会话仍有效(最长 7 天)——需应用侧改造 | +| 数据权限注入 | `CRUDBase` 的删除/清空/改状态路径未注入数据权限 | +| 需求规格错位 | 文档声称的 SaaS 多租户在代码中零实现 | +| Web i18n 与通知枚举 | 85/101 页面未接入 i18n;通知状态枚举与后端语义冲突 | +| 测试覆盖与夹具 | 夹具掩盖缺陷、迁移链路 0 覆盖(CI 已建立,但用例仍需补) | +| 移动端其它缺陷 | `alova.ts:115-119` 请求头写成 `ContentType`(缺连字符),修正需回归上传/表单/JSON 三类请求 | +| 密钥残留 | 旧数据库口令与 OPENAI_API_KEY 仍存在于 git 历史,建议按已泄露处理并重新签发 | + +### 9.3 本轮产生的回滚资产 + +- 镜像:`backend:prev`(上一版)、`nginx:rollback-1.25` +- 配置备份:`docker-compose.yaml.bak-*`、`nginx.conf.bak-*`、`.env.bak-*` +- 静态成品备份:`backups/{web,app-h5}-<时间戳>`(每类保留 5 份) +- 种子账号新口令:服务器 `/root/.fa-seed-passwords`(600,登录后请修改并删除) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f53337a2..f55961b4 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -108,7 +108,22 @@ ss -tlnp | grep docker-proxy # 确认对外只有 80/443 --- -## 6. 已知遗留问题 +## 6. 配置取值格式(踩过的坑) + +`.env` / compose 里的取值格式取决于应用侧字段类型: + +| 类型 | 写法 | 例子 | +| --- | --- | --- | +| `str`(应用内部再按逗号拆) | 逗号分隔字符串 | `PROD_CORS_ORIGINS=https://a.com,https://b.com` | +| `list[str]`(pydantic-settings 直接解析) | **必须 JSON 数组字面量** | `OAUTH_ALLOWED_HOSTS=["service.fastapiadmin.com","*.fastapiadmin.com"]` | + +把 `list[str]` 字段写成逗号分隔字符串会让应用**启动期抛 `SettingsError`**,容器不断重启、网关返回 502。2026-10-04 的部署事故就是这个原因(已修复并在 compose 默认值里固化 JSON 形态)。 + +排查方式:`docker logs --tail 40 backend`,若看到 +`pydantic_settings.exceptions.SettingsError: error parsing value for field "XXX" from source "EnvSettingsSource"`, +就是该字段的取值格式与类型不匹配。 + +## 7. 已知遗留问题 安全与质量债的完整清单见 `AUDIT-REPORT.md` 与各分报告(`backend/audit-*.md`、`frontend/audit-*.md`、`docker/audit-deploy.md`)。当前仍待处理的高优先级项: diff --git a/docker/.env.example b/docker/.env.example index df7e5623..eb0dd4cd 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -45,6 +45,19 @@ PROD_CORS_ORIGINS=https://service.fastapiadmin.com FORWARDED_ALLOW_IPS=10.255.178.0/24 # 演示模式:true 时禁止写操作;生产需要可写请改为 false DEMO_ENABLE=true +# Host 头校验范围:false 时 ALLOWED_HOSTS 只保留生产域名(不并入 localhost)。 +# 容器健康检查已配置为发送正确的 Host,因此生产建议 false; +# 若你的健康检查/探针确实以 Host: localhost 访问容器,再设为 true。 +ALLOW_LOCALHOST_HOSTS=false +# OAuth 回调与换取令牌允许的 Host。 +# ⚠️ 该字段在应用里是 list[str]:必须写 **JSON 数组**,写成逗号分隔字符串会让服务启动失败 +# (pydantic-settings 解析报 SettingsError,容器会不断重启)。 +# 逗号分隔只适用于 str 类型字段,例如上面的 PROD_CORS_ORIGINS。 +OAUTH_ALLOWED_HOSTS=["service.fastapiadmin.com","*.fastapiadmin.com"] +# 生产未配置 PROD_CORS_ORIGINS 时是否直接启动失败:false=仅告警(默认),true=严格 +CORS_STRICT_STARTUP=false +# 定时任务是否允许执行用户提交的代码块(等同 RCE 能力,生产保持 false) +SCHEDULER_ALLOW_CODE_EXEC=false # ---------- Nginx ---------- HTTP_PORT=80 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8e68443e..7b0f10a9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -128,6 +128,17 @@ services: PROD_CORS_ORIGINS: "${PROD_CORS_ORIGINS:-}" # 演示模式:为 true 时禁止写操作。当前线上为 true,如需可写请设为 false DEMO_ENABLE: "${DEMO_ENABLE:-true}" + # Host 头校验范围:false 时 ALLOWED_HOSTS 收敛为仅生产域名(推荐)。 + # 容器健康检查已改为发送正确 Host,因此这里可以为 false。 + ALLOW_LOCALHOST_HOSTS: "${ALLOW_LOCALHOST_HOSTS:-true}" + # OAuth 回调/换取令牌允许的 Host。 + # 注意:应用里该字段是 list[str],pydantic-settings 要求 **JSON 数组**字面量, + # 写成逗号分隔字符串会导致启动期 SettingsError(已踩过,服务会起不来)。 + OAUTH_ALLOWED_HOSTS: '${OAUTH_ALLOWED_HOSTS:-["service.fastapiadmin.com","*.fastapiadmin.com"]}' + # 生产未配置 PROD_CORS_ORIGINS 时是否直接启动失败(默认 false:仅告警并按空列表处理) + CORS_STRICT_STARTUP: "${CORS_STRICT_STARTUP:-false}" + # 定时任务是否允许执行用户提交的代码块(等同 RCE 能力,生产保持 false) + SCHEDULER_ALLOW_CODE_EXEC: "${SCHEDULER_ALLOW_CODE_EXEC:-false}" ports: # 仅绑定本机:数据面不对公网开放,容器之间经 app_network 互联 - "127.0.0.1:${BACKEND_PORT:-8001}:8001" @@ -147,11 +158,14 @@ services: condition: service_healthy networks: - app_network + # 健康检查必须带正确的 Host 头:应用启用了 TrustedHostMiddleware, + # Host 为 localhost 会被判 400;这里用「发正确 Host」而不是「放宽 ALLOWED_HOSTS」, + # 避免为了容器自检而降低对外 Host 校验强度。 healthcheck: test: [ "CMD-SHELL", - "python -c 'import urllib.request; req = urllib.request.Request(\"http://localhost:8001/api/v1/monitor/health/check\", headers={\"X-Forwarded-Proto\": \"https\"}); urllib.request.urlopen(req, timeout=5)' || exit 1", + "python -c 'import urllib.request; req = urllib.request.Request(\"http://localhost:8001/api/v1/monitor/health/check\", headers={\"Host\": \"service.fastapiadmin.com\", \"X-Forwarded-Proto\": \"https\"}); urllib.request.urlopen(req, timeout=5)' || exit 1", ] interval: 15s timeout: 10s