本篇将主要介绍user namespace和其余类型的namespace的关系。html
权限涉及的范围很是广,因此致使user namespace比其余的namespace要复杂; 同时权限也是容器安全的基础,因此user namespace很是重要。node
本篇全部例子都在ubuntu-server-x86_64 16.04下执行经过linux
除了user namespace外,建立其它类型的namespace都须要CAP_SYS_ADMIN的capability。当新的user namespace建立并映射好uid、gid了以后, 这个user namespace的第一个进程将拥有完整的全部capabilities,意味着它就能够建立新的其它类型namespaceshell
#先记下默认的user namespace编号 dev@ubuntu:~$ readlink /proc/$$/ns/user user:[4026531837] #用非root帐号建立新的user namespace dev@ubuntu:~$ unshare --user -r /bin/bash root@ubuntu:~# readlink /proc/$$/ns/user user:[4026532463] #虽然新user namespace的root帐号映射到外面的dev帐号 #但仍是能建立新的ipc namespace,由于当前bash进程拥有所有的capabilities root@ubuntu:~# cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)' CapInh: 0000000000000000 CapPrm: 0000003fffffffff CapEff: 0000003fffffffff root@ubuntu:~# readlink /proc/$$/ns/ipc ipc:[4026531839] root@ubuntu:~# unshare --ipc /bin/bash root@ubuntu:~# readlink /proc/$$/ns/ipc ipc:[4026532469]
固然咱们也能够不用这么一步一步的建立,而是一步到位ubuntu
dev@ubuntu:~$ readlink /proc/$$/ns/user user:[4026531837] dev@ubuntu:~$ readlink /proc/$$/ns/ipc ipc:[4026531839] dev@ubuntu:~$ unshare --user -r --ipc /bin/bash root@ubuntu:~# readlink /proc/$$/ns/user user:[4026532463] root@ubuntu:~# readlink /proc/$$/ns/ipc ipc:[4026532469]
在unshare的实现中,就是传入了CLONE_NEWUSER | CLONE_NEWIPC,大体以下安全
unshare(CLONE_NEWUSER | CLONE_NEWIPC);
在上面这种状况下,内核会保证CLONE_NEWUSER先被执行,而后执行剩下的其余CLONE_NEW*,这样就使得不用root帐号而建立新的容器成为可能,这条规则对于clone函数也一样适用。bash
Linux下的每一个namespace,都有一个user namespace和他关联,这个user namespace就是建立相应namespace时进程所属的user namespace,至关于每一个namespace都有一个owner(user namespace),这样保证对任何namespace的操做都受到user namespace权限的控制。这也是上一篇中为何sethostname失败的缘由,由于要修改的uts namespace属于的父user namespace,而新user namespace的进程没有老user namespace的任何capabilities。app
这里能够看看uts namespace的结构体,里面有一个指向user namespace的指针,指向他所属于的user namespace,其余类型的namespace也相似。dom
struct uts_namespace { struct kref kref; struct new_utsname name; struct user_namespace *user_ns; struct ns_common ns; };
在系统中,有些须要特权操做的资源没有跟任何user namespace关联,好比修改系统时间(须要CAP_SYS_MODULE)、建立设备(须要CAP_MKNOD),这些操做只能由initial user namespace里有相应权限的进程来操做(这里initial user namespace就是系统启动后的默认user namespace)。函数
当和mount namespace一块儿用时,不能挂载基于块设备的文件系统,可是能够挂载下面这些文件系统
#摘自user namespaces帮助文件: #http://man7.org/linux/man-pages/man7/user_namespaces.7.html * /proc (since Linux 3.8) * /sys (since Linux 3.8) * devpts (since Linux 3.9) * tmpfs (since Linux 3.9) * ramfs (since Linux 3.9) * mqueue (since Linux 3.9) * bpf (since Linux 4.4)
示例
#建立新的user和mount namespace dev@ubuntu:~$ unshare --user -r --mount bash root@ubuntu:~# mkdir ./mnt #查找挂载到根目录的设备 root@ubuntu:~# mount|grep " / " /dev/mapper/ubuntu--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered) #确认这个设备确定是块设备 root@ubuntu:~# ls -l /dev/mapper/ubuntu--vg-root lrwxrwxrwx 1 nobody nogroup 7 7月 30 11:22 /dev/mapper/ubuntu--vg-root -> ../dm-0 root@ubuntu:~# file /dev/dm-0 /dev/dm-0: block special (252/0) #尝试把他挂载到其余目录,结果挂载失败,说明在新的user namespace下无法挂载块设备 root@ubuntu:~# mount /dev/mapper/ubuntu--vg-root ./mnt mount: /dev/mapper/ubuntu--vg-root is write-protected, mounting read-only mount: cannot mount /dev/mapper/ubuntu--vg-root read-only #即便是root帐号映射过去也不行(这里mount的错误提示的好像不太准确) root@ubuntu:~$ exit exit dev@ubuntu:~$ sudo unshare --user -r --mount bash root@ubuntu:~# mount /dev/mapper/ubuntu--vg-root ./mnt mount: /dev/mapper/ubuntu--vg-root is already mounted or /home/dev/mnt busy /dev/mapper/ubuntu--vg-root is already mounted on / #因为当前pid namespace不属于当前的user namespace,因此挂载/proc失败 root@ubuntu:~# mount -t proc none ./mnt mount: permission denied #建立新的pid namespace,而后挂载成功 root@ubuntu:~# unshare --pid --fork bash root@ubuntu:~# mount -t proc none ./mnt root@ubuntu:~# mount |grep mnt|grep proc none on /home/dev/mnt type proc (rw,nodev,relatime) root@ubuntu:~# exit exit #只能经过bind方式挂载devpts,直接mount报错 root@ubuntu:~# mount -t devpts devpts ./mnt mount: wrong fs type, bad option, bad superblock on devpts, missing codepage or helper program, or other error In some cases useful info is found in syslog - try dmesg | tail or so. root@ubuntu:~# mount --bind /dev/pts ./mnt root@ubuntu:~# mount|grep mnt|grep devpts devpts on /home/dev/mnt type devpts (rw,nosuid,noexec,relatime,mode=600,ptmxmode=000) #sysfs直接mount和bind mount都不行 root@ubuntu:~# mount -t sysfs sysfs ./mnt mount: permission denied root@ubuntu:~# mount --bind /sys ./mnt mount: wrong fs type, bad option, bad superblock on /sys, missing codepage or helper program, or other error In some cases useful info is found in syslog - try dmesg | tail or so. #TODO: 对于sysfs和devpts,和帮助文件中描述的对不上,不肯定是我理解有问题,仍是测试环境有问题,等之后有新的理解后再来更新 # 挂载tmpfs成功 root@ubuntu:~# mount -t tmpfs tmpfs ./mnt root@ubuntu:~# mount|grep mnt|grep tmpfs tmpfs on /home/dev/mnt type tmpfs (rw,nodev,relatime,uid=1000,gid=1000) #ramfs和tmpfs相似,都是内存文件系统,这里就不演示了 #对mqueue和bpf不太熟悉,在这里也不演示了
当mount namespace和user namespace一块儿用时,就算老mount namespace中的mount point是shared而且用unshare命令时指定了--propagation shared,新mount namespace里面的挂载点的propagation type仍是slave。这样就防止了在新user namespace里面mount的东西被外面父user namespace中的进程看到。
#准备目录和disk dev@ubuntu:~$ mkdir -p disks/disk1 dev@ubuntu:~$ dd if=/dev/zero bs=1M count=32 of=./disks/disk1.img dev@ubuntu:~$ mkfs.ext2 ./disks/disk1.img #mount好disk,确认是shared dev@ubuntu:~$ sudo mount /home/dev/disks/disk1.img /home/dev/disks/disk1 dev@ubuntu:~$ cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 164 24 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 #先不建立user namespace,看看效果,便于和后面的结果比较, #当不和user namespace一块儿用时,新mount namespace中的挂载点为shared dev@ubuntu:~$ sudo unshare --mount --propagation shared /bin/bash root@ubuntu:~# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 220 174 7:1 / /home/dev/disks/disk1 rw,relatime shared:105 root@ubuntu:~# exit exit #建立mount namesapce的同时,建立user namespace, #能够看出,虽然指定的是--propagation shared,但获得的结果仍是slave(master:105) #因为指定了--propagation shared, 系统为咱们新建立了一个peer group(shared:154), #并让新mount namespace中的挂载点属于它, #这里同时也说明一个挂载点能够属于多个peer group。 dev@ubuntu:~$ unshare --user -r --mount --propagation shared /bin/bash root@ubuntu:~# cat /proc/self/mountinfo |grep disk| sed 's/ - .*//' 220 174 7:1 / /home/dev/disks/disk1 rw,relatime shared:154 master:105
在有一种状况下,没有CAP_SETUID的权限也能够写uid_map和gid_map,那就是在父user namespace中用新user namespace的owner来写,可是限制条件是只能在里面映射本身的帐号,不能映射其余的帐号。
在新user namespace中用有CAP_SETUID权限的帐号能够来写map文件,但跟上面的状况同样,只能映射本身。细心的朋友可能觉察到了,那就是都尚未映射,新user namespace里的帐号怎么有CAP_SETUID的权限呢?关于这个问题请参考下一节(建立新user namespace时capabilities的变迁)的内容。
因为演示第二种状况须要写代码(能够参考unshare的实现),这里就只演示第一种状况:
#--------------------------第一个shell窗口---------------------- #建立新的user namespace并记下当前shell的pid dev@ubuntu:~$ unshare --user /bin/bash nobody@ubuntu:~$ echo $$ 25430 #--------------------------第二个shell窗口---------------------- #映射多个失败 dev@ubuntu:~$ echo '0 1000 100' > /proc/25430/uid_map -bash: echo: write error: Operation not permitted #只映射本身成功,1000是dev帐号的ID dev@ubuntu:~$ echo '0 1000 1' > /proc/25430/uid_map #设置setgroups为"deny"后,设置gid_map成功 dev@ubuntu:~$ echo '0 1000 1' > /proc/25430/gid_map -bash: echo: write error: Operation not permitted dev@ubuntu:~$ cat /proc/25430/setgroups allow dev@ubuntu:~$ echo "deny" > /proc/25430/setgroups dev@ubuntu:~$ cat /proc/25430/setgroups deny dev@ubuntu:~$ echo '0 1000 1' > /proc/25430/gid_map dev@ubuntu:~$ #--------------------------第一个shell窗口---------------------- #回到第一个窗口后从新加载bash,显示当前帐号已是root了 nobody@ubuntu:~$ exec bash root@ubuntu:~# id uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
上面写了"deny"到文件/proc/[pid]/setgroups, 是为了限制在新user namespace里面调用setgroups函数来设置groups,这个主要是基于安全考虑。考虑这样一种状况,一个文件的权限为"rwx---rwx",意味着other group比本身group有更大的权限,当用setgroups(2)去掉本身所属的相应group后,会得到更大的权限,这个在没有user namespace以前不是个问题,由于调用setgroups须要CAP_SETGID权限,但有了user namespace以后,一个普通的帐号在新的user namespace中就有了全部的capabilities,因而他能够经过调用setgroups的方式让本身得到更大的权限。
clone函数 unshare函数 +----------------------------------+ +----------------------------------+ | 父进程 | 子进程 | | 当前进程 | |----------------------------------+ |----------------------------------+ | 启动 | | 启动 | | | ① | | | ① | | ↓ | | ↓ | | clone ----------→ 启动 | | unshare | | | | ② | | | ② | | | ↓ | | ↓ | | | ④ exec | | exec | | | | ③ | | | ③ | | ↓ ↓ | | ↓ | | 结束 结束 | | 结束 | +----------------------------------+ +----------------------------------+
上面描述了进程在调用不一样函数后所处的不一样阶段,clone函数会建立新的进程,unshare不会。
①: 处于父user namespace中,进程拥有的capabilities由调用该进程的user决定
②: 处于子user namespace中,这个时候进程拥有所在子user namespace的全部capabilities,因此在这里能够写当前进程的uid/gid map文件,但只能映射当前帐号,不能映射任意帐号。这里就回答了上一节中为何没有映射帐号但在子user namespace有CAP_SETUID权限的问题。
③: 处于子user namespace中,调用exec后,因为没有映射,系统会去掉当前进程的全部capabilities(这个是exec的机制),因此到这个位置的时候当前进程已经没有任何capabilities了
④: 处于父user namespace中,和①同样。但这里是一个很好的点来设置子user namespace的map文件,若是能在exec执行以前设置好子进程的map文件,exec执行完后当前进程仍是有相应的capabilities。可是若是没有在exec执行以前设置好,而是exec以后设置,当前进程仍是没有capabilities,就须要再次调用exec后才有。如何在④这个地方设置子进程的map文件须要一点点技巧,能够参考帮助文件最后面的示例代码。
对于unshare来讲,因为没有④,全部无法映射任意帐号到子user namespace,这也是为何unshare命令只能映射当前帐号的缘由。
和pid namespace相似,当在程序中用UNIX domain sokcet将一个user namespace的uid或者gid发送给另外一个user namespace中的进程时,内核会自动映射成目的user namespace中对应的uid或者gid。