本篇將主要介紹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。