把散落在代码里的真实部署值统一收进 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 未被误建)。
10 KiB
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 |
事故 / 踩坑记录
🔴 ① 我用 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,
断言改为「常量必须为空」(把「不留生产域名」变成回归项)。
回滚 / 注意
# 代码回滚:删 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 个失败属既有问题,与本次无关,未修。