# 140-涉密内容外置到配置目录(2026-09-19 落地) > **一句话**:把散落在代码里的**真实部署值**(域名 / IP / 主机号 / 内网路径 / Worker 令牌 / PG 口令) > 统一收进 `config/`,代码改为引用配置;`src/`+`web/`+`test/` **功能性真实标识 = 0**。 ## 背景与动机 **用户原话**:「很多涉密内容包含在代码中 开源时非常容易泄密,把所有涉密内容统一整理到配置文件夹对应文件中, 代码中引用对应配置信息」;收口时追加:「改造后记得**完整检查两遍**」。 改造前依赖**导出层脱敏**(`_build_export.py` 的 71 条 `GLOBAL` + 73 条 `REGEX_RULES` + 75 条 `LEAK_PROBES`) 把生产值替换成占位符 —— 那是**事后擦除**,规则漏一条就泄一条(导出技能事故 #17 就是这么来的: 已公开仓库里躺着 7 个含内网主机号与 PG 口令的脚本)。本次改为**事前分离**:真实值根本不在代码里。 ## 用户决策 | 事项 | 决定 | |---|---| | 真实值放哪 | 新建 `config/` 目录,真实值进 `config/platform.env`(**.gitignore 排除、不进开源导出**) | | 代码怎么取 | shell 走 `config/load.sh`;node 脚本走 `config/index.cjs`;TS 走 `src/platform-paths.ts` | | 缺配置时的行为 | **中性默认值**(不含任何真实路径/域名)+ shell 侧发告警;⛔ 不把生产值留作代码默认 | | 注释里的真标识 | **中性化**(`` / `` / ``),注释无法「引用配置」 | | 测试夹具 | 改 RFC 2606/5737 保留值(`example.net` / `203.0.113.10` / `w-1`·`w-2`) | ## 实现 ### A. 新增配置层(5 文件) | 文件 | 入库 | 作用 | |---|---|---| | `config/platform.env.example` | ✅ | 模板(全占位符) | | `config/platform.env` | ⛔ gitignore | 本机真实值;服务器侧由其自身 systemd env 生成(600) | | `config/load.sh` | ✅ | shell 加载器:**系统 env > platform.env > 中性默认**;派生 `DSH_STATE_DIR`/`_BACKUP_DIR`/`_ARTIFACT_DIR` | | `config/index.cjs` | ✅ | node 脚本加载器(零副作用,同优先级口径) | | `config/README.md` | ✅ | 键一览 + 用法 + 优先级 | `.gitignore` 增补:`config/platform.env` · `config/*.env.local`。 ### B. TS 侧:单一来源 + 去生产值 - **新增 `src/platform-paths.ts`** —— 部署相关路径的**唯一解析处**(零副作用,不建目录/不写文件): `dataRootDir/platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath` ⚠️ 刻意**不**调 `resolveConfig()`(它会 mkdir 数据根并可能生成 `secret.key`)。 - `src/config.ts`:新增 `platformDir/stateDir/backupDir/artifactDir/installDir` 字段; **`DEFAULT_OVERLAY_BOOTSTRAP_SEEDS` 由 `['https://<生产域>/dshs-relay']` 改为 `[]`**。 - `src/net/relay/directory.ts`:**`DEFAULT_OVERLAY_SEED` 由生产 URL 改为 `''`**(`overlayEnvSeeds()` 未配 ⇒ 返回空)。 - 4 处硬编码绝对路径改为引用:`web/home-files.ts`(backups)· `web/server.ts`(state)· `web/routes/admin.ts`(`scriptPath('ensure-biz-plugins.cjs')` + `stateDir()/runtime-baseline.json`)· `web/routes/dsh.ts`(capabilities)· `web/routes/overlay-nodes.ts`(`dataRootDir()/overlay/nodes.json`)· `net/relay/registry.ts`(`DSH_RELAY_LIB_DIR ?? installDir()`)。 - 注释中性化:`src/**` 116 行 / 53 文件(仅注释行,零行为风险)。 ### C. `scripts/` 侧(内部运维脚本,36 文件) - 真凭据 → 配置:`W47_TOKEN`/`W106_TOKEN` → `$DSHS_CLUSTER_AGENT_TOKEN`/`$DSH_PEER_AGENT_TOKEN`; PG 口令 → `$DSHS_PG_PASSWORD`;隧道目标 → `$DSHS_TUNNEL_TARGET`。 - 路径/地址/主机号 → 配置:`/opt/dshs`→`$DSH_INSTALL_DIR`、`/var/lib/dshs`→`$DSHS_DATA_ROOT`、 `/opt/dsh/*`→`$DSH_(STATE|BACKUP|ARTIFACT)_DIR`、`47.77.182.89`→`$DSH_HOST_PUBLIC_IP`、 `w-47`/`w-106`→`$DSHS_CLUSTER_HOST_ID`/`$DSH_PEER_HOST_ID`。 - 🔴 **两处必须手改的形态**(脚本守卫刻意跳过): ① **引号定界 heredoc**(`<<'NFTEOF'` / `<<'ENVEOF'`)内变量**不展开** ⇒ 替换会产出字面量 `"$VAR"`: `install-egress-guard.sh` 改用 `__HOST_ADDRS__` 占位符 + 写完文件后 `sed` 注入; `switch-B2-dropin.sh` 的 heredoc 是**非引号**定界(可展开)⇒ 直接参数化。 ② **单引号内的 ssh 载荷**(`ssh h 'mkdir -p /var/lib/dshs'`)⇒ 改双引号本地展开后下发。 - `scripts/dshlog.mjs` 的主机表(含真 IP 与 ssh 别名)改为读 `DSHLOG_HOSTS`(JSON)。 ### D. `web/` 与 `test/` 侧 - `web/wake.html` 的 `safeNext()`:**旧的 `*.ai1net.com` 白名单正则**改为运行时从 `location.hostname` 推导注册域(去最左一段)⇒ 换域名零改动(导出技能 §3「特例」的既定口径)。 - `test/**` 夹具 **119 行 / 13 文件**:生产域名 → `example.net`/`example.org`(RFC 2606)、 真 IP → RFC 5737 文档段、`w-47`/`w-106` → `w-1`/`w-2`、`/var/lib/dshs` → `/var/lib/dsh-test`。 - `test/overlay-bootstrap.test.mjs` 的**语义断言**同步更新: 由「内置种子 == 生产 URL」改为「**内置种子必须为空**」(防止生产域名再被写回代码)。 ### E. 提交 / 部署 | 项 | 值 | |---|---| | 备份 | `/opt/dsh/backups/pre-secrets-config-20260919-150752/lib`(294 文件) | | 部署 | `config/` → `/opt/dshs/config/`(服务器侧从自身 env 生成 `platform.env`,**32 键 / 600**);`lib/` → `/opt/dshs/lib`(297 文件) | | 新增 drop-in | `/etc/systemd/system/dshs.service.d/platform-dirs.conf`:`DSH_PLATFORM_DIR=/opt/dsh` · `DSH_INSTALL_DIR=/opt/dshs` | | 重启 | `systemctl restart dshs`(开发环境,无需先知会) | ## 验证记录 | 项 | 结果 | |---|---| | `tsc -p tsconfig.json --noEmit` | **exit 0** | | `npm test`(375 用例) | **373 pass / 1 fail / 1 skip**;唯一失败 = `lease: 释放后归零…`,**既有失败**(重建 lib 前的旧产物上就是同一处) | | 全部改动 `.sh` | `bash -n` **全通过** | | 全部改动 node 脚本 | `node --check` **全通过** | | 全仓扫描(大小写不敏感) | `src/`+`test/`+`scripts/`+`web/`+`assets/`+`config/` **= 0 命中** | | 部署后派生路径 | `/opt/dsh/state`、`/opt/dsh/backups` 仍在原处;**`/var/lib/dshs/platform` 未被误建**(零回归判据) | | `capabilities.json` / `runtime-baseline.json` / `ensure-biz-plugins.cjs` / `overlay nodes.json` | 均仍在原路径 ✅ | | 线上端点 | `portal.html`/`login.html`/`admin.html`/`favicon.svg` **200**;`/api/admin/users` 未认证 **401** | | 单元 | 47 `dshs`/`dshs-pg`/`dshs-worker` **active**;近 3 分钟 `ENOENT|Cannot find module|TypeError` **0** | ## 事故 / 踩坑记录 ### 🔴 ① 我用 `git stash` 做基线比对,被 SIGTERM 打断 ⇒ **`.git/refs` 被删、仓库不可识别** - **现象**:`git rev-parse` 报 `not a git repository`;`.git/refs` 目录**不存在**,`packed-refs` 也没有。 - **取证**:`.git/objects/pack/*.pack` **完好**(`verify-pack` 正常列出提交);**三处 reflog** (`logs/HEAD`、`logs/refs/heads/master`、`logs/refs/remotes/origin/master`)末行**全部收敛于 `9c2e7975aca484463afd5f2a36e5484222f040c0`**,且与本轮开机时实测的本地 HEAD / `origin/master` 一致。 - **恢复**:① 先 `tar czf` 保全工作树(2.6 MB);② 由 reflog 重建 `refs/heads/master` 与 `refs/remotes/origin/master`;③ `git fetch origin` 取回缺失对象(pack 里缺最新 commit); ④ `git reset`(mixed,不碰工作树)重建 index;⑤ `git fsck` **干净**、77 项改动全部正常可见。 - **教训(已固化)**:**⛔ 不要用 `git stash` 做「临时回到基线」** —— 它是写操作且不可中断; 要基线比对就用 `git worktree add` 或在导出副本里 checkout。 **先落 patch 备份**(本次 `/tmp/mysrc.patch` 与工作树 tar 包救了场)。 ### ② 我的「注释中性化」正则曾把 ASCII 标点吃进去(本轮未发生,但踩到了它的同类) 清理规则只准碰**全角标点**(本轮实现里已限定「只处理整行注释」,规避了该类事故)。 ### ③ 「字符串内被打入」两处 **由测试抓出** `verify-cluster-domain.mjs` / `verify-platform-admin-section.mjs` 里 `'test.ai1net.com'` 这类 **引号内字面量**被映射规则打进 `cfg.get(...)` ⇒ JS 语法错。 ⇒ **教训**:映射不能只按「行」判断,必须按**字面量是否在字符串/引号内**判断; **改完必须逐文件 `node --check`**(两处就是靠它抓到的)。 ### ④ 大小写敏感漏扫 `register-guard.test.mjs` 里的 `'https://AI1net.com/'`(大写 `AI`)逃过第一轮扫描, **由测试断言失败暴露**。⇒ 扫描一律 `re.I`。 ### ⑤ 内置种子不是「可清空的常量」而是**测试夹具的依赖** 清空 `DEFAULT_OVERLAY_SEED` 后,3 个用例跟着红(`B1`/`B7`/`S0`)—— 它们把该常量当**合法公网 URL 夹具**用。修法 = 让夹具自带 `FIXTURE_SEED`, **断言改为「常量必须为空」**(把「不留生产域名」变成回归项)。 ## 回滚 / 注意 ```bash # 代码回滚:删 config/ 改动即可(纯新增目录 + 引用改造),或 git -C /d/github/dsh_shenxian checkout -- src/ scripts/ web/ test/ # 服务器回滚: rm /etc/systemd/system/dshs.service.d/platform-dirs.conf && systemctl daemon-reload cp -a /opt/dsh/backups/pre-secrets-config-20260919-150752/lib /opt/dshs/lib systemctl restart dshs # config/ 目录留着无害(不被旧代码引用) ``` - ⚠️ **`config/platform.env` 是新的「单一秘密副本」** ⇒ 必须保持 `600`、保持 gitignore; **开源导出层必须显式排除 `config/platform.env`**(导出白名单是 `INCLUDE_DIRS`,`config/` 目前不在其中; 若要随包发模板,只能加 `platform.env.example`)。 - ⚠️ 服务器侧 `platform.env` 由 `systemctl show dshs -p Environment` 生成 ⇒ **drop-in 改动后要重新生成**,否则脚本读到旧值。 - 🔴 **drop-in 里必须保留 `DSH_PLATFORM_DIR=/opt/dsh`** —— 删掉它,`platformDir` 会退化成 `/platform` = `/var/lib/dshs/platform`,于是 state/backups/artifacts **静默搬家**。 - ⏳ 遗留:`test/lease.test.mjs` 的那 1 个失败属**既有**问题,与本次无关,未修。