1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
9.3 KiB
39-实例可见面收窄(/etc 白名单 + 宿主访问封锁)(2026-09-11 落地)
TL;DR|结论:按用户要求收窄实例可见面:
/etc改白名单,并封127.0.0.0/8等四类目的地址(宿主 loopback、内网卡、docker0、公网 EIP)。 关键:nft H5 规则;/usr刻意保留(运行时就住在/usr/local,去掉实例起不来)。 状态:✅ 已落地(其引致的 python3 回归由档案 42 修复)
背景与动机
用户要求:「用户就只能访问(含读取)dsh 服务用户 id 对应的那个文件夹,不能读取其他路径(连读都不要读取)」。
(用户明确澄清「用户路径」= <dataRoot>/users/<该用户id>/,不是 /usr。)
起因:档案 38a 发现 bundled-skills 未挂载;进一步审计暴露两个问题:
/etc整体--ro-bind→ 实例能读走 576 个 others-readable 文件,其中含平台情报。- bwrap 只
--unshare-pid不--unshare-net→ 实例与宿主共享网络命名空间,可枚举/连接宿主服务。
用户决策
| 决策点 | 用户选择 | 理由 |
|---|---|---|
执行 /etc 白名单化 |
✅ 执行 | 已实测零功能损失 |
| 一并收窄网络面(宿主访问封锁) | ✅ 执行 | 才是真正的越权读取通道 |
移除 /usr 可见面 |
❌ 不执行(采纳建议) | 运行时就住在 /usr/local,去掉 = 实例起不来;/usr/bin 的 1151 个 CLI 是 agent 唯一手脚;审计证实 /usr 零凭据、零用户数据 |
实现
A. bwrap /etc 收窄(src/supervisor/orchestrator.ts,spawnAsUser)
'--ro-bind', '/etc', '/etc' → '--tmpfs', '/etc' + 逐文件 --ro-bind-try。
白名单 14 项(代码内 allow 列表):ld.so.cache、ld.so.conf、passwd、group、nsswitch.conf、
hosts、resolv.conf、host.conf、services、localtime、os-release、machine-id、
pki/tls/certs、pki/ca-trust。
⚠️ symlink 必须
realpathSync()后绑「目标 → symlink 原路径」,否则实例内是断链 (/etc/nsswitch.conf→/etc/authselect/nsswitch.conf;/etc/localtime→/usr/share/zoneinfo/Asia/Shanghai)。 新增realpathSync导入。
B. nft H5 宿主访问封锁(/etc/nftables-dsh-egress.nft)
在 ip dsh_egress 表的 output 链新增,仅作用于 skuid 100000-199999:
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, 172.18.16.212, 47.77.182.89 } \
meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn counter reject with tcp reset
meta skuid 100000-199999 ip daddr { …同上… } meta l4proto udp ct state new counter reject
封 4 类目的地址:127.0.0.0/8(宿主全部 loopback 服务 + 其他实例的 127.0.0.1 监听)、
172.18.16.212(eth0 内网卡)、172.17.0.1(docker0 网关)、47.77.182.89(公网 EIP)。
与平台既有 src/supervisor/firewall.ts 的 portGuard(iptables -m owner ! --uid-owner 0 -j REJECT)
语义一致、范围更全,且不依赖 config.portGuard 开关。portGuard 的存在反证了
「实例进程自身不需要任何 loopback 连接」。
验证记录
修改前对照实验(同用户 admin,仅 /etc 变量不同)
| 探针 | C0(整体绑定) | C1(白名单) |
|---|---|---|
node -v |
v22.23.2 | v22.23.2 |
os.userInfo() |
OK | OK |
/etc/passwd 可读 |
OK | OK |
DNS lookup |
OK | OK |
| node TLS → registry.npmjs.org | 200 | 200 |
curl https://www.baidu.com |
200 | 200 |
dsh --profile web --dump-config |
544 行 / 170 插件 | 544 行 / 170 插件(逐字一致,exit=0) |
修改后终验(guest uid 100002,收窄后沙箱)
| 项 | 结果 |
|---|---|
/etc 可见项 |
222 → 13 |
/etc 可读文件 |
576 → 27 |
| 平台情报 systemd / nft 护栏 / cron / letsencrypt | 4/1/2/8 → 0/0/0/0 |
| 其他用户目录可见 | 1 项(仅自己) |
| 数据库可读 | NO |
| node / userInfo / DNS / 出网 TLS / curl 公网 | 全 OK |
dsh --profile web --dump-config |
549 行 / 176 插件,exit=0,stderr 空 |
dsh --version |
0.1.2-rc.1 |
| loopback :3080 / :22 / :888 / :58888 | 全部 BLOCKED |
| 宿主 IP :443 / 内网卡 :80 / 元数据端点 | 全部 BLOCKED |
| nft HOST 规则计数器 | 命中 1 包 |
| 门户健康 | systemctl is-active = active,3080 → 200 |
真实启动 E2E(orchestrator 原样命令 + 收窄后沙箱)
[workspace-scoped-picker] loaded root=…/ws (cwd)
dsh web: http://127.0.0.1:41999/?token=… ← 打印 launch token(= orchestrator 成功判据)
- root(模拟 nginx 回源)→ 实例端口:TCP 连接成功 +
HTTP/1.1 303 See Other+set-cookie: dsh-auth-…(正常登录跳转,实例完全可用) - 实例内(uid 100002)→ 自己端口:BLOCKED ECONNREFUSED(H5 生效)
事故 / 踩坑记录
★ P0 级自伤:H5 按「目的地址」一刀切会打断实例回包(实测踩到并修复)
- 现象:H5 首版只写
ip daddr { 127.0.0.0/8, … } reject,部署后 root 连实例端口 超时(curl000、node net.connecttimeout),TCP 层就不通。 - 根因:实例是「先收后回」的服务端。nginx(root) → 实例的 SYN 合法,但实例回的 SYN-ACK 与后续数据包,目的地址同样是
127.0.0.1(客户端就在本机)→ 被同一条 daddr 规则拒掉 → 握手永远完不成。 - 修正:只匹配实例主动发起的连接 —— TCP 加
tcp flags & (fin|syn|rst|ack) == syn(纯 SYN;SYN-ACK 带 ack 位故不匹配),UDP 加ct state new。修后 root→实例 303 正常,实例内自连仍 BLOCKED。 - 教训(通用):在 OUTPUT 链按目的地址封本机服务时,必须区分「主动发起」与「回包」;仅靠 daddr 会连回包一起封。判定特征是 timeout(丢包)而非 refused——refused 说明规则生效且只在客户端方向;timeout 往往意味着双向都被打到。
- 幸运点:修复时无实例在跑(
systemctl list-units "dsh-*scope"= 0),线上用户零影响。
其他
- ⚠️ 分析脚本漏设
DSH_HOME会让dsh --dump-config输出 1 行(找不到 profile),不是回归——对比测试必须与基线同 env。 - ⚠️ 档案号二次撞号已从「串行取号」升级为「原子预留」(
mkdir .lock-<n>),本次占号 39 一次成功。
回滚 / 注意
- 代码:
cp src/supervisor/orchestrator.ts.bak-<ts> src/supervisor/orchestrator.ts && npm run build && systemctl restart dshs - nft:
cp /etc/nftables-dsh-egress.nft.bak-<ts> /etc/nftables-dsh-egress.nft && systemctl restart dsh-egress(或整体停用:systemctl disable --now dsh-egress) - 副作用:
/etc在白名单外改为 tmpfs(可写、临时),实例退出即消失,无跨租户影响。 - 未做(残留):
--unshare-net(会断外网,不建议);/usr收窄(判定无收益)。 - 后续:
bundled-skills挂载(档案 38a P0)仍未修,需在本改动之上追加--ro-bind-try。
事后修正(2026-09-11 17:15):白名单漏了 /etc/alternatives → 21 个命令静默断链
发现途径:分析 guest「代码库功能与权限探索」会话时,agent 独立报告「python3 不存在」。
我起初以为是 agent 误报(宿主上 /usr/bin/python3 明明存在),复核后确认是我引入的回归。
根因:/usr/bin/python3 是两跳软链——
/usr/bin/python3 → /etc/alternatives/python3 → /usr/bin/python3.6
中间一跳在 /etc,而白名单只绑了「叶子文件的 realpath 目标到原路径」,
没有绑 /etc/alternatives 这个「枢纽目录」 → 沙箱内该目录不存在 → 软链断裂。
受损清单(21 条,通用探测器枚举):
python3 python pip3 pip-3 pydoc3 pydoc-3 python3-config pyvenv-3
easy_install-3 unversioned-python ld→ld.bfd pax
print-*(lp lpr lpq lprm lpstat cancel)ifup ifdown lpc
其中对我方场景关键的是 python3、pip3(装技能依赖)、ld(node-gyp 编译原生模块)。
修法:白名单追加 /etc/alternatives 整目录。安全性已核:其 33 项全部指向 /usr 或 /lib64 下的程序/man(已只读挂载),不含任何凭据或平台情报 → 不扩大实质可见面。实测修后 /etc 可见项 13→14、可读文件 27、平台情报仍 0/0/0/0、其他用户目录仍 1 项、DB 不可读。
⚠️ 通用教训(已写进 skill):/etc 白名单化时,必须枚举所有"符号链接链条会穿过 /etc"的条目,不能只看直接指向 /etc 的那些。探测器:
find /usr/bin /usr/sbin /usr/libexec /usr/local/bin -maxdepth 1 | while read -r f; do
[ -L "$f" ] || continue; c="$f"; n=0
while [ -L "$c" ] && [ $n -lt 10 ]; do t=$(readlink "$c")
case "$t" in /*) c="$t";; *) c="$(dirname "$c")/$t";; esac
case "$c" in /etc/*) echo "$f -> $c";; esac; n=$((n+1)); done
done | sort -u
另:这类回归不会报错、只会静默 command not found,只能靠"改动前后对比工具清单"或会话取证发现 —— 佐证了「会话取证」作为验收手段的价值。