Files
dsh_shenxian/dsh-server-docs/04-调整方案/38a-实例软件安装共享与网络安边界核查.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
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 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

15 KiB
Raw Blame History

38a · 实例内软件安装、共享复用可行性、网络与安全边界核查

  • 日期:2026-09-11
  • 触发:用户问「推荐安装 Playwright 和其他软件包,是每个用户都要单独安装一份吗?能否复用?主要排查功能限制(如联网、DNS 等)以及安全性相关问题」
  • 状态:🔍 核查完成(未改码)
  • 关联:档案 10/11(共享技能层)、档案 14(出网护栏)、档案 16/17/18/23(沙箱与可见面)、档案 37a(guest 会话取证)

TL;DR|结论:回答"每个用户是否要各装一份":宿主 /usr 已 ro-bind → 宿主装一次、全部实例共享;同时暴露最大隐患 = 实例与宿主共享网络命名空间(可访问 127.0.0.1:3080/22 等)。 关键:结论:软件走宿主层(rg/jq/ffmpeg、Playwright 系统库同理);网络面问题指向档案 39。 状态:🔍 核查完成(未改码)


一、结论速览

问题 结论
每个用户要单独装一份吗? 是。HOME=<userRoot>/ws 每用户独立,pip/npm 全落在自己工作区;实例间互不可见
能复用吗? 能,且有一条零改造通道:宿主 /usr 已 --ro-bind 进实例(含 /usr/local)→ 宿主装一次 = 全员生效
Playwright 能装吗? 当前完全不能(缺 11 个系统 .so,/usr 只读 + 无 sudo);要让它能用必须在宿主层装系统库
联网 全开:仅封云元数据端点(实测 Connection refused),其余只记日志不拦。DNS 走阿里云内网 DNS,可用
最大安全隐患 ①实例与宿主共享网络命名空间(可访问 127.0.0.1:22/80/443/3080、可枚举宿主所有监听、可 bind 0.0.0.0 跨租户互通)②磁盘无配额(单用户可写满 40G 打瘫全平台)③并发上限仅 2–3 个实例(宿主 1870MB / 每实例 512M)
新 P0 bundled-skills 在实例命名空间内不存在 → 档案 10/11 的「全员共享只读技能层」实际失效(见 §二)

二、新 P0 · bundled-skills 在实例内不存在 → 共享技能层实际失效(修正档案 10/11)

证据链

  1. config.ts:245-247:bundledSkillDir = overrides ?? env.DSHS_BUNDLED_SKILL_DIR ?? join(dataRoot,'bundled-skills');/etc/dshs.env 未设该变量 → 实际值 = /var/lib/dshs/bundled-skills。
  2. orchestrator.ts:363:DSH_BUNDLED_SKILL_DIR 已注入实例 env ✅(环境变量这层没问题)。
  3. orchestrator.ts:454-486 的 bwrap 参数只挂 /usr、/lib64、/etc、/dev、/proc、<userRoot>/tmp、<userRoot>、profile 三个只读文件 —— 没有任何一条 --ro-bind 覆盖 bundled-skills。
  4. 以平台原样参数实测(guest uid,见 §八):
$ ls /var/lib/dshs/
users                                   ← 只有 bwrap 合成的 users,其余全是空的合成父目录
$ ls /var/lib/dshs/bundled-skills
ls: cannot access '...': No such file or directory
$ echo $DSH_BUNDLED_SKILL_DIR
/var/lib/dshs/bundled-skills
  1. dsh-skill-filesystem/lib/index.js:84,181-183 是实例内直接读盘,不是由编排器推送数据:
const bundledSkillDir = config.bundledSkillDir
  ?? (this.includeDefaultRoots ? process.env.DSH_BUNDLED_SKILL_DIR : void 0);
this.bundledSkillDir = bundledSkillDir === void 0 ? void 0 : resolve(bundledSkillDir);
...
if (this.bundledSkillDir !== void 0) roots.push({ path: this.bundledSkillDir, source: "bundled" })

→ 结论:即使把技能投进 /var/lib/dshs/bundled-skills,实例端也扫不到,技能永远不会被发现。 这修正了档案 10(「已实施 2026-09-09」)与档案 11 的生效判断 —— 机制代码写完了,但被 bwrap 挂载面切断。也说明档案 37a 的 P0-2「零技能投放」不只是"没投",而是投了也无效,必须先修挂载。

修法:bwrapArgs 在 --bind root root 之后补一条 '--ro-bind-try', this.config.bundledSkillDir, this.config.bundledSkillDir (用 -try 容忍目录暂不存在;放在 --bind root root 之后以免被覆盖)。 通用规则(本次新增):平台侧任何新增的共享目录,都必须同步加 bwrap --ro-bind,否则实例内不可见 —— 这是本架构最容易漏的一环。


三、每用户一份 vs 复用:现状与三条路径

现状(为什么是每用户一份)

环节 实测值
HOME <userRoot>/ws(每用户独立)
pip 缓存/安装 ws/.cache/pip、ws/.local/lib/python3.6/site-packages
npm 缓存 ws/.npm/{_cacache,_logs}
磁盘实占 guest 13M / admin 47M(重复下载的包各自一份)
跨用户可见 ❌ bwrap 只暴露 <userRoot>,看不到别人的

三条复用路径对比

方案 做法 改动量 生效面 评价
A. 宿主 /usr/local(推荐首选) 装到宿主 /usr/local/{bin,lib,share} 零代码 —— /usr 已 ro-bind 全员、所有实例、新用户自动 实测实例内可见 /usr/local/bin(含 dsh/npm/pnpm/node)与 /usr/local/lib/node_modules。唯一注意:全局单版本、升级要动宿主
B. dataRoot 共享只读目录 建 <dataRoot>/shared-runtime/ + bwrap 加 --ro-bind + env 白名单(PLAYWRIGHT_BROWSERS_PATH/PYTHONPATH/PATH) 改 orchestrator.ts + spawn.ts ALLOWED_ENV 全员 与 bundled-skills 同构。必须先修 bind,否则重蹈 §二
C. 每用户一份 + 共享缓存 保留现状,把 pip/npm 指向私有镜像/共享缓存 无 无 只降重复下载,不降磁盘占用,且仍污染工作区

附带发现:PLAYWRIGHT_BROWSERS_PATH 默认落在 $HOME/.cache/ms-playwright(就在用户工作区里)→ 与 ws/.npm、ws/.cache/pip 同类,会被门户 #/files 展示、也可能被 ws-cleanup 触碰。


四、功能限制清单(实测)

4.1 网络 / DNS

项 实测 判定
DNS 服务器 100.100.2.136 / 100.100.2.138(阿里云内网 DNS) 可用,解析成功 ✅
/etc/resolv.conf 可改? /etc 是 ro-bind → touch /etc/x = Read-only file system 用户改不了 DNS ✅
外网出站 https://www.baidu.com → 200;pip / npm 均可下载 全开 ⚠️
云元数据端点 http://100.100.100.200/ → Connection refused(nft reject + log prefix "dsh-egress-BLOCK") 已封 ✅
出网护栏性质 /etc/nftables-dsh-egress.nft:只 reject 元数据端点;其余 counter ... log prefix "dsh-egress" level info 只记录不拦截(tcp/udp,排除 127/8 与 53) 观测 ≠ 管控 ⚠️
网络命名空间 实例 net:[4026531994],bwrap 只 --unshare-pid,未 --unshare-net 与宿主共享 🔴
127.0.0.1 可达 3080(门户) OPEN、22(SSH) OPEN、80 OPEN、443 OPEN 🔴
宿主监听枚举 实例内 /proc/net/tcp 能看到宿主全部 LISTEN(实测 8 条) 🔴 内网信息泄露
自建监听 bind 0.0.0.0:ephemeral OK、bind 127.0.0.1 OK 🔴 见 §五

4.2 文件系统

路径 权限 说明
/usr、/lib64、/etc 只读(ro-bind) touch /usr/local/lib/x → Read-only file system
/bin、/sbin、/lib 符号链接 → usr/{bin,sbin,lib}(合成) 档案 23 的修复
<userRoot>、<userRoot>/ws 可写,属主 = 用户 uid 工作区
<userRoot>/tmp 1777,每用户独立 bind 到 /tmp ✅ 隔离正确
<userRoot>/home/profiles/web/{cordis.patch.yml,package.json,pnpm-lock.yaml} 额外的只读覆盖 ✅ 防绕过平台策略
/etc/passwd 可读(30 行) ⚠️ 可枚举平台账号名(低危)
宿主 /usr/bin 可读,1151 个二进制 共享通道(也是攻击面)

4.3 资源上限(systemd-run --scope)

项 值 备注
MemoryMax 512M / 实例
CPUQuota 150% 宿主仅 2 vCPU → 单实例可占 75%
TasksMax 128
宿主内存 1870 MB(swap 1024M) → 并发上限约 2–3 个实例(512M×3 已 1.5G)🔴
宿主磁盘 40G ext4,可用 29G,单分区
磁盘配额 未启用(quotaon / → not found) 🔴 单用户可写满 40G
实例内 df / tmpfs 936M(bwrap 合成根,非宿主盘) 借宿主机内存池

五、安全边界评级

成立(实测有效) ✅

  1. uid 隔离:实例内 uid=100002(或 114801),非 root;sudo 不可用。
  2. 路径隔离:看不到其他用户目录(ls users/ → Permission denied)、看不到 DB、看不到宿主凭据;/usr /etc 只读。
  3. 元数据端点封禁:实测 refused。
  4. 平台策略文件只读:cordis.patch.yml / package.json / pnpm-lock.yaml 无法被用户改写(防绕过投放管控)。
  5. /tmp 每用户独立 + 1777 sticky。
  6. 会话全文扫描:明文凭据 0 命中(档案 37a)。

缺口 ⚠️

# 缺口 后果 等级
S1 共享宿主网络命名空间 ① 可访问宿主 127.0.0.1:22(SSH 爆破/探测)② 可访问 127.0.0.1:3080 门户 ③ /proc/net/tcp 枚举宿主监听 ④ bind 0.0.0.0 后其他租户与宿主均可访问 → 跨租户横向通道 P0
S2 磁盘无配额 单租户写满 40G → 编排器/DB/所有实例全挂(DoS) P0
S3 并发容量仅 2–3 实例 1870MB 内存硬顶;第 4 个用户进场即 OOM 风险 P1
S4 出网"只记录不拦" 数据外传、SSRF、挖矿;日志有 skuid 覆盖但无告警、无阈值 P1
S5 用户可 pip/npm install 任意包 供应链;且包落在工作区(污染 + 占用) P1
S6 /etc/passwd 可读 枚举 30 个账号名 P2
S7 宿主 /usr 全量可见(1151 个二进制 + 全局 node 包) 攻击面扩大(本地提权利用链的探测素材) P2

修法方向(按性价比)

  • S1:给 bwrap 加 --unshare-net + 每实例独立的 loopback(--unshare-net 会自带 lo,但需 ip link set lo up)→ 实例失去对外网访问,需配套 DNS/代理方案;折中版:保留出网,但用 nft meta skuid 规则拒绝实例访问 127.0.0.1:22/3080 等宿主服务端口(成本最低、收益最直接,不破坏联网能力)。
  • S2:--property=MemoryMax 已有 → 加 --property=IOWeight/DevicePolicy 不够;正解是给 <userRoot> 上项目配额(ext4 project quota,需 remount 加 prjquota)或限制 ws 大小 + 定期 ws-cleanup 兜底。
  • S3:加全局并发闸门(同时实例数上限)+ 空闲回收(档案 30 已有部分)。
  • S4:把 log 升级为计数阈值告警;对已知恶意目标(挖矿池常见域名/IP 段)改 reject。

六、Playwright 专项:为什么"装不起来",以及正确做法

用户问"是不是每人装一份" —— 前提不成立,当前装不起来:

要素 实测 结论
浏览器二进制 可下载到 $HOME/.cache/ms-playwright(ws 内可写) 二进制这层没问题
系统共享库 缺 11 个(libnss3.so/libgbm.so.1/libatk-1.0.so.0/libcups.so.2/libdrm.so.2/libxkbcommon.so.0 …) ❌ 致命
补库的途径 /usr、/lib64 只读;yum/dnf 在但无 sudo ❌ 用户侧无解
Chromium 大小 ~300MB/份 若每人一份,×N 且无磁盘配额

正确做法(一次装、全员用):

  1. 宿主层 yum install 那 11 个库 → 因 /usr 已 ro-bind,所有实例立即可见,零代码改动;
  2. 浏览器二进制放宿主公共位(如 /usr/local/share/ms-playwright);
  3. 通过 env 注入 PLAYWRIGHT_BROWSERS_PATH 指向公共位(需同时加 spawn.ts ALLOWED_ENV + orchestrator.ts baseEnv,见档案 10 的 env 白名单卡点)。

但需先回答"值不值得"(档案 37a P1-1 已论证):抖音签名墙只挡「更全」,不挡「够用」;而 MCN 真正要的播放量/完播/涨粉/带货数据 Playwright 拿不到。→ 建议优先级:RedFox 数据源接入 > Playwright。

其他软件包同理:rg/jq/ffmpeg 也是宿主装一次、全员共享(走 /usr/local 或 /usr/bin),不要引导用户自己装 —— 用户自己装既装不上(只读 + 无 sudo),装上了也是 N 份重复。


七、建议行动项

# 行动 类型 说明
1 bwrap 补 --ro-bind bundledSkillDir(§二 P0) 改码 修完技能投放才真正生效;否则档案 37a P0-2 白做
2 nft 拒绝实例访问 127.0.0.1:22/3080(S1 折中版) 改配置 成本最低、直接堵住最大安全缺口,不破坏联网
3 给用户目录上配额或大小兜底(S2) 改码/运维 防单租户打瘫全平台
4 公共软件统一走宿主层(rg/jq/ffmpeg/Playwright 系统库) 运维 一次装全员共享,别让用户自己装
5 全局并发闸门 + 出网阈值告警(S3/S4) 改码 容量与观测

八、实测命令记录(可复用)

# ① 权威 bwrap 参数(从失败的 scope 里直接读,比读源码更可靠)
systemctl list-units "dsh-*" --no-pager --all | grep scope

# ② 以平台原样参数进实例做只读诊断(把 diag 脚本 ro-bind 进 /tmp 即可,不动用户目录)
B=/usr/bin/bwrap; R=/var/lib/dshs/users/<UID>
$B --ro-bind /usr /usr --ro-bind /lib64 /lib64 \
   --symlink usr/bin /bin --symlink usr/sbin /sbin --symlink usr/lib /lib \
   --ro-bind /etc /etc --dev /dev --proc /proc \
   --bind $R/tmp /tmp --bind $R $R \
   --ro-bind /tmp/diag.sh /tmp/diag.sh \
   --unshare-pid --chdir $R/ws \
   -- setpriv --reuid <uid> --regid <uid> --clear-groups /bin/sh /tmp/diag.sh

# ③ 资源上限 / 宿主容量
free -m; nproc; df -h /; quotaon -p /

# ④ 出网护栏实况
nft list table ip dsh_egress; cat /etc/nftables-dsh-egress.nft

# ⑤ 元数据端点是否真的被封(从实例 uid 发起)
setpriv --reuid 100002 --regid 100002 --clear-groups \
  curl -sS -m 8 -o /dev/null -w '%{http_code}\n' http://100.100.100.200/latest/meta-data/

本次踩坑:pgrep -f "dsh --profile web" 会匹配到我自己的 bash 命令行(因为命令串里含该模式)→ 拿到的是假 PID。改用 ps -eo pid,user,cmd | grep -E "^ *[0-9]+ .*node /usr/local/bin/dsh --profile web" 或直接读 systemctl list-units "dsh-*"。