最近有一个需求,就是要在服务器端执行用户传过来的命令。为了防止用户运行一些恶意指令,我们需要构造一个沙箱来运行。这种隔离的需求第一反应就想到了之前写过的 使用 overlayfs 和 chroot 搭建临时环境,但是这需要 root 权限。有没有办法在不需要 root 的情况运行 mount
和 chroot
呢?简单搜了下就找到了想要的答案。
unshare
是 linux 下的一个命令,通过构造一个新的 namespace 来和现有的环境隔离。基本用法很简单,就是一些选项,然后再加上要运行的命令即可,看起来有点像一个精简版的 docker。而且和 docker 一样的是,只要运行的命令退出了,在命令运行期间做的操作都会被自动清除。这个完美符合我们对沙箱的要求。
先来做个测试。通过 unshare
把当前用户映射成 root,并创建一个新的 mount namespace
:
unshare -rm /bin/bash
运行之后发现我们的用户变成了 root!另外一个不容易发现的内容就是,在我们所创建的这个 mount namespace
中进行 mount
和 umount
操作,不会影响外面的真实的文件系统。也就是说如果我们在这个环境中 unmount 了一个挂载点,退出进程(也就是我们这里的 bash)之后这个挂载点还在,并没有被 unmount。但是除此之外的其它操作(例如增删文件)都会被保留下来。这也是 unshare
这个名字的由来:只有指定的选项才会被 unshare,没有指定的都还是 share 的。
稍微解释下 -rm
这两个选项。-r
表示将当前用户映射成 root 用户,-m
就是刚提到的 mount namespace
。
又因为是沙箱环境,我们不希望用户发现系统中正在运行的其它进程,所以我们可以创建一个 pid namespace
:
unshare -rpf /bin/bash
这里我们多加了 -f
选项,表示使用 fork()
来运行我们的程序(这里的 /bin/bash
),如果不加这个选项会报错:
bash: fork: Cannot allocate memory
这个错误在 pid_namespaces(7)
中有说明。
我们使用 -r
选项把自己映射成了 root,但只在新创建的 namespace 中有效。如果我们在隔离出来的 namespace 中尝试去删掉一个外部 root 权限的文件时仍然会报错,因为这两个不是同一个 namespace 内的 root,就相当于一个 namespace 中的用户不能操作另一个 namespace 中的文件,这样才是隔离的效果。
在执行最终的脚本之前,需要用 root 手动挂载 /dev
和 /sys
:
mount --bind /dev chroot-env/dev
mount -t sysfs sysfs chroot-env/sys
最终使用下面的脚本就可以实现我们的 chroot 需求:
#!/bin/bash
tmpfile=$(mktemp)
cat > $tmpfile <<EOF
export PATH=/usr/sbin:$PATH
mount --bind /home/ouonline/workspace chroot-env/home/ouonline/workspace
chroot chroot-env /bin/bash
EOF
unshare --mount --uts --fork --pid -map-root-user /bin/bash $tmpfile
rm $tmpfile
还有就是进入 chroot 环境之后还需要手动挂载一下 /dev/pts
和 /proc
:
mount -t proc proc /proc
mount -t devpts devpts /dev/pts
unshare 的 --mount-proc
好像不起作用。
chroot-env
目录可以是我们在用的系统的一个备份(参考 使用 tar 备份 linux),或者是用 debootstrap
获取的一个镜像:
debootstrap --arch amd64 sid chroot-env
为了保持开发环境目录的一致性,可以在进入临时环境之后手动执行一次
usermod -d /home/ouonline -u 0 root
把临时环境中的 root
的家目录改成真实用户的家目录。
FAQ
Q:如果执行 apt-get update
时遇到报错 setgroups 65534 failed - setgroups (1: Operation not permitted)
A:(参考资料 2)进入容器后执行
# apt-config dump | grep Sandbox::User
APT::Sandbox::User "_apt";
# cat <<EOF > /etc/apt/apt.conf.d/sandbox-disable
APT::Sandbox::User "root";
EOF
# apt-config dump | grep Sandbox::User
APT::Sandbox::User "root";