Linux Namespace系列(08):user namespace (CLONE_NEWUSER) (第二部分)

本篇将主要介绍user namespace和其余类型的namespace的关系。html

权限涉及的范围很是广,因此致使user namespace比其余的namespace要复杂; 同时权限也是容器安全的基础,因此user namespace很是重要。node

本篇全部例子都在ubuntu-server-x86_64 16.04下执行经过linux

和其余类型的namespace一块儿使用

除了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

和其余类型namespace的关系

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关联的资源

在系统中,有些须要特权操做的资源没有跟任何user namespace关联,好比修改系统时间(须要CAP_SYS_MODULE)、建立设备(须要CAP_MKNOD),这些操做只能由initial user namespace里有相应权限的进程来操做(这里initial user namespace就是系统启动后的默认user namespace)。函数

和mount 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

其余能够写map文件的状况

  1. 在有一种状况下,没有CAP_SETUID的权限也能够写uid_map和gid_map,那就是在父user namespace中用新user namespace的owner来写,可是限制条件是只能在里面映射本身的帐号,不能映射其余的帐号。

  2. 在新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的方式让本身得到更大的权限。

建立新user namespace时capabilities的变迁

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。

参考

相关文章
相关标签/搜索