feat(config): 涉密内容外置到配置目录(档案 140)

把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
This commit is contained in:
admin committed 2026-09-19 15:12:19 +08:00
1 parent 9c2e7975ac
commit 452924d89c
100 files changed
+1167 -453

No files matched your search

@@ -0,0 +1,152 @@
# 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 侧发告警;⛔ 不把生产值留作代码默认 |
| 注释里的真标识 | **中性化**(`<server-public-ip>` / `<base-domain>` / `<host-a>`),注释无法「引用配置」 |
| 测试夹具 | 改 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` 会退化成
`<dataRoot>/platform` = `/var/lib/dshs/platform`,于是 state/backups/artifacts **静默搬家**。
- ⏳ 遗留:`test/lease.test.mjs` 的那 1 个失败属**既有**问题,与本次无关,未修。