251 lines
9.8 KiB
Markdown
251 lines
9.8 KiB
Markdown
# 硬隔离教程(新手版)— 每用户独立 OS 账号
|
||||
|
|
|
|||
|
|
> 🧭 [← 返回 README](../README.md) · 前置:[基础部署](deployment.md) · 出问题:[排查手册](troubleshooting.md)
|
|||
|
|
|
|||
|
|
> 本教程是 [deployment.md](deployment.md) 的进阶篇。先按那篇把整套跑通、能正常登录和打开 DSH,再回来做这个。
|
|||
|
|
> 全文用 `example.com` 举例,请替换成你自己的域名;`/root/dshs` 换成你实际 `git clone` 的目录。
|
|||
|
|
|
|||
|
|
## 0. 为什么需要硬隔离(先看懂再动手)
|
|||
|
|
|
|||
|
|
默认部署是**软隔离**:所有用户的 DSH 进程跑在**同一个系统账号(root)**下,每个用户自己的目录设了 `0700`(只有本人能进)。
|
|||
|
|
|
|||
|
|
问题是:`0700` 只对**别的系统账号**有效。同一个 root 账号下的进程之间,**权限检查形同虚设**——一个用户(的 DSH 进程)可以用 root 直接读另一个用户的文件。
|
|||
|
|
|
|||
|
|
**硬隔离**就是:给每个用户建一个**独立的 Linux 系统账号**,让它的 DSH 进程以这个账号运行。这样:
|
|||
|
|
|
|||
|
|
- `0700` 才真正生效(别的账号进不来)。
|
|||
|
|
- 用户 A 的 DSH 进程,即使用户 B 的目录是 `0700`,也会被系统直接拒绝。
|
|||
|
|
|
|||
|
|
**代价**:需要 root 权限 + 每个用户创建时要做一次「建账号 + 改属主」。好在可以自动化(见 §3.1),一次配好就不用管。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 确认编排服务已用 root 运行
|
|||
|
|
|
|||
|
|
硬隔离要靠 `setpriv` 把进程降权到目标账号,这**只有 root 能调用**。所以编排服务必须以 root 跑。
|
|||
|
|
|
|||
|
|
如果你用了 [deployment.md](deployment.md) 的 systemd 配置(`/etc/systemd/system/dshs.service`),默认就是 root,跳过这步。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 打开账号级隔离的开关
|
|||
|
|
|
|||
|
|
在环境变量文件里加两个配置(`/etc/dshs.env`):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
# 追加这两行
|
|||
|
|
DSHS_ISOLATION_MODE=account
|
|||
|
|
DSHS_BASE_UID=100000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 变量 | 值 | 解释 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `DSHS_ISOLATION_MODE` | `account` | 开启账号级隔离(默认 `soft`)。 |
|
|||
|
|
| `DSHS_BASE_UID` | `100000` | 每个用户 uid 的「起始数字」。系统 uid 一般 < 1000,这里从 10 万开始,避免撞系统账号。 |
|
|||
|
|
|
|||
|
|
改完**重启编排服务**让配置生效:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
systemctl restart dshs
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> 从这一刻起,编排服务 spawn 每个用户的 DSH 时,会用它内置的 `setpriv` 命令(`setpriv --reuid <uid> --regid <uid> --inh-caps=-all --clear-groups --`)把进程降权到该用户账号。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2.1 ⚠️ 前置:`dsh` 必须装在降权账号能读到的位置(不能装在 `/root` 下)
|
|||
|
|
|
|||
|
|
硬隔离降权后,子 DSH 以每用户账号(uid > 100000)运行,这些账号**读不到 root 的 home 目录 `/root`**。
|
|||
|
|
|
|||
|
|
如果 `dsh`(以及它依赖的 node)是用 nvm 装在 `/root/.nvm/...` 下的(默认就是),开了 `account` 之后,子 DSH 一启动就会报 `Cannot find module '/root/.nvm/.../bin/dsh'`,然后**每秒崩溃循环**。日志长这样:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
[dsh-child main] node:internal/modules/cjs/loader:1210
|
|||
|
|
Error: Cannot find module '/root/.nvm/versions/node/.../bin/dsh'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**所以开 §2 的开关之前,先把 node + dsh 装到系统级位置**(所有账号都能读):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
# 1) 系统级安装 node 22(装到 /usr/bin)
|
|||
|
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|||
|
|
apt install -y nodejs
|
|||
|
|
|
|||
|
|
# 2) 系统级安装 dsh(注意:必须强制 prefix,否则会被 nvm 劫持装回 /root)
|
|||
|
|
NPM_CONFIG_PREFIX=/usr/local /usr/bin/npm install -g @deepseek-ai/dsh
|
|||
|
|
ls -l /usr/local/bin/dsh # 必须是 /usr/local/bin/dsh,不能在 /root 下
|
|||
|
|
|
|||
|
|
# 3) 环境变量指向系统 dsh
|
|||
|
|
# /etc/dshs.env → DSHS_DSH_BIN=/usr/local/bin/dsh
|
|||
|
|
|
|||
|
|
# 4) systemd 的 PATH 去掉 /root/.nvm/...,改成系统路径
|
|||
|
|
# (子 DSH 的 #!/usr/bin/env node 才找得到系统 node)
|
|||
|
|
# /etc/systemd/system/dshs.service:
|
|||
|
|
# Environment=PATH=/usr/bin:/usr/local/bin:/usr/sbin:/usr/local/sbin:/sbin:/bin
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **nvm 劫持 npm 的坑**:即使你用 `/usr/bin/npm`,它仍可能装到 `/root/.nvm/...`——因为 `/root/.npmrc` 里被 nvm 写了 `prefix=/root/.nvm/versions/node/...`。检查:`/usr/bin/npm prefix -g`(返回 nvm 路径就是被劫持了)。解决:命令前加 `NPM_CONFIG_PREFIX=/usr/local` 强制覆盖。
|
|||
|
|
|
|||
|
|
> **另一个降权后读不到的东西**:运行时插件 `dshs/runtime` 在 `git clone` 的目录(如 `/dsh_login/dshs/lib/`)里,降权账号也要能读到。检查 `ls -ld /dsh_login`,如果是 `drwx------`(只有 root),执行 `chmod 755 /dsh_login`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 为每个用户创建系统账号(核心一步)
|
|||
|
|
|
|||
|
|
这是**唯一需要手动做的事**:用户注册后,要先给他建系统账号 + 把他目录改成这个账号所有,硬隔离才完整。**没做这一步,DSH 会以 root 运行(跟没开硬隔离一样)。**
|
|||
|
|
|
|||
|
|
创建脚本(`provision-user.sh`,root 运行):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/usr/bin/env bash
|
|||
|
|
# 用法:provision-user.sh <userId> (userId 就是数据库里 users 表的 id,管理员界面能看到)
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
uid="$(dshs uid-for-user "$1")"
|
|||
|
|
user="dsh-$1"
|
|||
|
|
|
|||
|
|
# 1) 创建系统账号(uid 用插件算出来的同一个值,保证两边一致;不建 home、不能登录)
|
|||
|
|
useradd -u "$uid" -M -s /usr/sbin/nologin "$user"
|
|||
|
|
|
|||
|
|
# 2) 把该用户的目录改成这个账号所有(关键!)
|
|||
|
|
chown -R "$uid:$uid" "/var/lib/dshs/users/$1"
|
|||
|
|
|
|||
|
|
echo "provisioned $1 -> uid $uid"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
怎么执行:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
chmod +x provision-user.sh
|
|||
|
|
./provision-user.sh <userId>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**userId 从哪拿**:管理员登录后访问 `/api/admin/users`(返回 JSON 里每个用户的 `id` 字段),或直接查数据库 `users` 表。
|
|||
|
|
|
|||
|
|
### 3.1 让它自动跑(不用每次手动)
|
|||
|
|
|
|||
|
|
每个用户注册后都手动跑一次太麻烦,这里给几个**自动触发**方案,按你服务器的环境挑一个:
|
|||
|
|
|
|||
|
|
**方案 A:systemd 监控目录(最简单,无需额外软件)**
|
|||
|
|
|
|||
|
|
把上面那个脚本存成 `/usr/local/bin/provision-user.sh`,然后用 systemd 的路径监控:**每当某个用户的目录被创建(即注册成功),就自动跑一次脚本**。
|
|||
|
|
|
|||
|
|
`/etc/systemd/system/dsh-provision.path`:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
[Unit]
|
|||
|
|
Description=Watch new user dirs and provision OS accounts
|
|||
|
|
|
|||
|
|
[Path]
|
|||
|
|
PathExistsGlob=/var/lib/dshs/users/*/home
|
|||
|
|
Unit=dsh-provision.service
|
|||
|
|
|
|||
|
|
[Install]
|
|||
|
|
WantedBy=multi-user.target
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`/etc/systemd/system/dsh-provision.service`:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
[Unit]
|
|||
|
|
Description=Provision a newly registered user
|
|||
|
|
After=dshs.service
|
|||
|
|
|
|||
|
|
[Service]
|
|||
|
|
Type=oneshot
|
|||
|
|
# 找出刚注册、还没建账号的用户,挨个建
|
|||
|
|
ExecStart=/usr/local/bin/provision-new-users.sh
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`/usr/local/bin/provision-new-users.sh`(核心:遍历所有用户目录,没建账号的先建,然后**每次都 chown**):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/usr/bin/env bash
|
|||
|
|
set -euo pipefail
|
|||
|
|
for dir in /var/lib/dshs/users/*/; do
|
|||
|
|
[ -d "$dir" ] || continue
|
|||
|
|
id="$(basename "$dir")"
|
|||
|
|
user="dsh-$id"
|
|||
|
|
if ! id "$user" &>/dev/null; then
|
|||
|
|
# 账号不存在才建(uid 用插件算出来的同一个值,保证两边一致)
|
|||
|
|
uid="$(dshs uid-for-user "$id")"
|
|||
|
|
useradd -u "$uid" -M -s /usr/sbin/nologin "$user"
|
|||
|
|
fi
|
|||
|
|
uid="$(id -u "$user")" # 账号已存在就复用它的 uid
|
|||
|
|
chown -R "$uid:$uid" "$dir" # 每次都 chown(幂等,把漏掉的属主补上)
|
|||
|
|
echo "provisioned $id -> uid $uid"
|
|||
|
|
done
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> ⚠️ **别写「账号已存在就跳过」**:那样会连 chown 一起跳过——如果某用户的目录后来变成 root 所有,重跑也不会修,他的 DSH 还是会因为读不了自己 home 而崩(外网 502)。所以 chown 要放在判断**外面**、每次都执行。
|
|||
|
|
|
|||
|
|
启用:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
systemctl daemon-reload
|
|||
|
|
systemctl enable --now dsh-provision.path
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这样以后**用户一注册,目录一出现,systemd 就自动建账号 + 改属主**,管理员不用再管。
|
|||
|
|
|
|||
|
|
**方案 B:定时任务(简单粗暴,隔几分钟扫一次)**
|
|||
|
|
|
|||
|
|
如果不想用 systemd 的 path 监控,就用 cron 每 5 分钟跑一遍同一个 `provision-new-users.sh`(幂等,跑多少次都安全):
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
crontab -e
|
|||
|
|
# 加一行:
|
|||
|
|
*/5 * * * * /usr/local/bin/provision-new-users.sh
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**方案 C:手动(用户少时够用)**
|
|||
|
|
|
|||
|
|
用户少、注册不频繁,管理员有空就手动跑 `./provision-user.sh <userId>` 也行。**但别忘了**——漏一个 = 那个用户还在 root 下跑,等于没隔离。
|
|||
|
|
|
|||
|
|
> **推荐**:方案 A 最省心、自动、幂等,一次配好就不用管。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 验证硬隔离真的生效
|
|||
|
|
|
|||
|
|
**第 1 步:看 DSH 进程的 uid。**
|
|||
|
|
|
|||
|
|
先在桌面启动一个用户的 DSH,然后:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
# 找到 DSH 进程
|
|||
|
|
ps aux | grep dsh | grep -v grep
|
|||
|
|
|
|||
|
|
# 假设拿到了 pid,看它的真实 uid
|
|||
|
|
ps -o uid,user,cmd -p <pid>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**应该看到**:`uid` 等于 `dshs uid-for-user <那个userId>`(是一个 >100000 的账号),**不是** 0(root)。如果显示 0,说明没降权,回头检查第 2、3 步。
|
|||
|
|
|
|||
|
|
**第 2 步:越权读测试(最关键)。**
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
# 1) 在用户 A 的 home 里放个文件
|
|||
|
|
echo "secret" > /var/lib/dshs/users/<A的id>/home/s.txt
|
|||
|
|
chmod 600 /var/lib/dshs/users/<A的id>/home/s.txt
|
|||
|
|
|
|||
|
|
# 2) 用用户 B 的 DSH 去读它(在 B 的 DSH 里执行 cat)
|
|||
|
|
# 正确结果:Permission denied(读不到)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
看到 `Permission denied` = 硬隔离生效。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 常见问题
|
|||
|
|
|
|||
|
|
| 现象 | 原因 / 处理 |
|
|||
|
|
|---|---|
|
|||
|
|
| DSH 还是以 root 跑 | `ISOLATION_MODE=account` 没生效(重启了吗?)、或该用户没跑 provision-user.sh。 |
|
|||
|
|
| `setpriv: no permission` 报错 | 编排服务没以 root 运行。 |
|
|||
|
|
| 新用户启动 DSH 报权限错误 | 该用户目录 chown 了吗?重新跑 provision-user.sh。 |
|
|||
|
|
| 忘了给某个用户做第 3 步 | 补跑,然后重启该用户的 DSH。 |
|
|||
|
|
| DSH 每秒崩溃,日志 `Cannot find module '/root/.nvm/.../bin/dsh'` | `dsh` 装在 `/root` 下,降权账号读不到。按 §2.1 把 dsh 装到系统级位置。 |
|
|||
|
|
| `/usr/bin/npm install -g` 还是装到 `/root/.nvm` | npm 全局前缀被 nvm 劫持(`/root/.npmrc` 里的 `prefix`)。用 `NPM_CONFIG_PREFIX=/usr/local` 强制覆盖。 |
|
|||
|
|
|
|||
|
|
---
|