User namespaces are have been introduced as early as Linux 3.5 and are considered as stable starting with Linux 4.3.

1. 简介

user namespace是最近才出现在内核主干里的,主要是为了隔离安全相关的标识和属性,例如 user IDs and group IDs (see credentials(7)), the root directory, keys (see keyctl(2)), and capabilities等等。但是从内核实现上来看,user namespace只是简单的提供了一种uid/gid映射机制,capabilities虽然与user namespace非常相关,但它不是user namespace中的概念,capabilities是进程的概念,它是进程的一种属性,它要远比user namespace出现的早

在这篇文章里,我们分四步走:

  1. 我们先来了解一下,user namespace的基本用法
  2. 以及user namespace如何结合capabilities实现容器的安全性隔离
  3. user namespace在解决什么问题?
  4. 内核实现
  5. 与其他namespace的交互以及兼容性问题

简单来说,有了这个东西之后,容器可以有一个假的root账户,在不影响宿主安全的情况下,最大限度的使用更多的内核特权功能,user namespace让容器变得更像虚拟机了

2. 基本用法

2.1. user namespace

user namespace主要实现了uid/gid的映射,操作系统启动的时候,内核会为操作系统初始化一个叫initial user namespace,这个namespace是所有后续user namespace的根【注意和其他namespace不一样,user namespace是支持嵌套的,后面会简单解释一下】

举个简单的例子来说:假如我们创建了一个namespace A,那么我们可以将A中的uid=0映射到initial namespace中的一个普通账号例如work,那么我们的进程就可以在A namespace中拥有root账户权限,但是在宿主系统中是个普通权限(这样做有什么好处?或者解决什么问题?详看capabilities一章)

参考文档:

http://man7.org/linux/man-pages/man7/user_namespaces.7.html

https://lwn.net/Articles/532593/

2.1.1. 创建

和其他所有namespace的创建方式一样,创建一个新的user namespace,你只需要在clone进程的时候,指定CLONE_NEWUSER参数即可

(This flag first became meaningful for clone() in Linux 2.6.23, the current clone() semantics were merged in Linux 3.5, and the final pieces to make the user namespaces completely usable were merged in Linux 3.8.)

代码如下:

child_pid = clone(childFunc, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, argv[1]);

创建出来的子进程就在一个新的user namespace里

另外,如果想进入其他namespace的话,可以利用unshare()/setns()系统调用,或者unshare工具、nsenter工具【ubuntu 16.04等比较新的操作系统支持】

2.1.2. uid映射与gid映射

当创建user namespace之后,一开始是没有任何uid/gid映射的

注意:映射是只子user namespace到父user namespace的映射,打破父子关系的映射目前是不支持的

内核提供的映射接口是proc文件系统里的两个文件:

  • The /proc/[pid]/uid_map and /proc/[pid]/gid_map files (available since Linux 3.5) expose the mappings for user and group IDs inside the user namespace for the process pid.

但是需要注意的是,这个文件是只能写一次

大部分情况下,uid_map和gid_map的配置都是一样的,所以下面不对这两个文件分开介绍

The uid_map file exposes the mapping of user IDs from the user namespace of the process pid to the user namespace of the process that opened uid_map (but see a qualification to this point below). In other words, processes that are in different user namespaces will potentially see different values when reading from a particular uid_map file, depending on the user ID mappings for the user namespaces of the reading processes.

当user namespace刚创建的时候,uid_map文件是空的,文件格式如下:

id-inside-ns id-outside-ns length

一个uid_map文件可能包含一行或者多行上述格式的内容,其中三个字段分别表示:

  1. 子user namespace【也就是clone之后起的子进程所在的新的user namespace】的起始uid
  2. 父user namespace【也就是调用clone函数的进程所在的user namespace】的起始uid
  3. 映射的长度

示例如下:

0 1000 500

表示,子user namespace的uid=0映射到父user namespace的uid=1000,子user namespace的uid=1映射到父user namespace的uid=1001,以此类推,长度为500,

但是第二个字段的值怎么显示,取决于打开uid_map的进程pid和uid_map所属的pid是否在同一个user namespace里,http://man7.org/linux/man-pages/man7/user_namespaces.7.html 里的解析不是很清楚,我简单说下:

  1. 如果两个进程都是同一个user namespace下的,或者打开uid_map的进程所在的user namespace是uid_map文件的进程所在的user namespace的父亲,那么这两个进程看到的uid_map内容是一致的
  2. 还有一种比较特殊的情况是,如果AB两个user namespace都是initial user namespace的子user namespace,那么B中的进程去看A中进程的uid_map,会是什么结果?结果是看到的第二个字段不一样,这里面有更深层次的转换关系,有兴趣的可以自行研究下

2.2. user namespace与credentials

有了uid/gid映射之后,一些获取进程uid或者gid的系统调用,将会返回namespace里的uid,而不再是宿主【或者父namespace】里的uid

更多请参考:

http://man7.org/linux/man-pages/man7/credentials.7.html

2.3. user namespace与文件系统

我们知道,文件系统是会记录uid/gid的,因为内核需要根据文件的owner、group信息判断进程是否具备相应的权限

但是打开user namespace之后,不同user namespace下看到的进程uid是不一致的,如果进程需要读写文件,内核如何做权限检查??

When a process accesses a file, its user and group IDs are mapped into the initial user namespace for the purpose of permission checking and assigning IDs when creating a file. When a process retrieves file user and group IDs via stat(2), the IDs are mapped in the opposite direction, to produce values relative to the process user and group ID mappings.

内核会在进程读写文件系统的时候,会根据进程的credentials信息以及user namespace的映射关系,层层转换,直到找到进程在initial user namespace中对应的uid/gid,这个才是真正持久化到文件系统里的

以docker为例,我们来看看,打开user namespace之后,容器内看到的文件owner信息以及在机器上看到的owner信息是什么样的关系

1)重启docker daemon,并指定userns-remap参数

$ sudo docker daemon --userns-remap=work

2)启动docker容器

sudo docker run -i --tty ubuntu /bin/bash

3)容器内看到的文件系统信息

drwxr-xr-x  34 root   root    4096 Jan 21 08:28 ./
drwxr-xr-x  34 root   root    4096 Jan 21 08:28 ../
-rwxr-xr-x   1 root   root       0 Jan 21 08:28 .dockerenv*
drwxr-xr-x   2 root   root    4096 Jan 19 16:33 bin/
drwxr-xr-x   2 root   root    4096 Apr 12  2016 boot/
drwxr-xr-x   5 root   root     380 Jan 21 08:28 dev/
drwxr-xr-x  45 root   root    4096 Jan 21 08:28 etc/
drwxr-xr-x   2 root   root    4096 Apr 12  2016 home/
drwxr-xr-x   8 root   root    4096 Sep 13  2015 lib/
drwxr-xr-x   2 root   root    4096 Jan 19 16:32 lib64/
drwxr-xr-x   2 root   root    4096 Jan 19 16:31 media/
drwxr-xr-x   2 root   root    4096 Jan 19 16:31 mnt/
drwxr-xr-x   2 root   root    4096 Jan 19 16:31 opt/
dr-xr-xr-x 290 nobody nogroup    0 Jan 21 08:28 proc/
drwx------   2 root   root    4096 Jan 19 16:33 root/
drwxr-xr-x   6 root   root    4096 Jan 20 21:43 run/
drwxr-xr-x   2 root   root    4096 Jan 20 21:43 sbin/
drwxr-xr-x   2 root   root    4096 Jan 19 16:31 srv/
dr-xr-xr-x  13 nobody nogroup    0 Jan 17 16:43 sys/
drwxrwxrwt   2 root   root    4096 Jan 19 16:33 tmp/
drwxr-xr-x  11 root   root    4096 Jan 20 21:43 usr/
drwxr-xr-x  13 root   root    4096 Jan 20 21:43 var/

4)机器上看到的原始的文件系统信息

p# ll /var/lib/docker/100000.100000/aufs/diff/08942b9816238413b083753503458d1cf7185c0f5397bbeeb6221afda5a0164b/
total 84
drwxr-xr-x 21 100000 100000 4096 1月  21 15:09 ./
drwx------ 13 100000 100000 4096 1月  21 16:00 ../
drwxr-xr-x  2 100000 100000 4096 1月  20 00:33 bin/
drwxr-xr-x  2 100000 100000 4096 4月  13  2016 boot/
drwxr-xr-x  4 100000 100000 4096 1月  20 00:32 dev/
drwxr-xr-x 42 100000 100000 4096 1月  20 00:33 etc/
drwxr-xr-x  2 100000 100000 4096 4月  13  2016 home/
drwxr-xr-x  8 100000 100000 4096 9月  13  2015 lib/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:32 lib64/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:31 media/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:31 mnt/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:31 opt/
drwxr-xr-x  2 100000 100000 4096 4月  13  2016 proc/
drwx------  2 100000 100000 4096 1月  20 00:33 root/
drwxr-xr-x  5 100000 100000 4096 1月  20 00:32 run/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:33 sbin/
drwxr-xr-x  2 100000 100000 4096 1月  20 00:31 srv/
drwxr-xr-x  2 100000 100000 4096 2月   5  2016 sys/
drwxrwxrwt  2 100000 100000 4096 1月  20 00:33 tmp/
drwxr-xr-x 10 100000 100000 4096 1月  20 00:31 usr/
drwxr-xr-x 11 100000 100000 4096 1月  20 00:33 var/

可以看到,容器内root权限读写的文件,最终都是以10000这个账号,持久化到文件系统里的

2.3.1.subuid & subgid

前面我们看到,docker启动的时候,打开user namespace的参数是--userns-remap=work

docker daemon --userns-remap=work

这个选项开了之后,docker所有启动的容器,容器内的root账户,都会映射到机器subuid中定义的work对应的范围,如下:

$ cat /etc/subuid
work:100000:65536

需要注意的是,所谓subuid,subgid,都是操作系统层面的概念,并不是内核层面的概念,如果你想了解更多,可以自行google看下

换句话说,如果你直接针对内核编程,那么完全可以不用关心subuid/subgid这些概念,你怎么映射、映射到哪个范围都是可以的

https://success.docker.com/Datacenter/Apply/Introduction_to_User_Namespaces_in_Docker_Engine

https://blog.yadutaf.fr/2016/04/14/docker-for-your-users-introducing-user-namespace/

2.4. user namespace与capabilities

有了user namespace之后,容器可以拥有一个fake root账户,但是这个所谓的root只是容器内部的root,在内核看来,它仍然是一个普通用户

fake root能做哪些事情?

  1. 是容器内文件系统的绝对owner。这就意味着,用户可以在容器内使用root账户安装很多app,例如apt-get install vim

fake root做不了哪些事情?

  1. 修改内核参数,例如sysctl,修改/proc文件系统
  2. 挂载,例如挂载procfs,sysfs,甚至是tmpfs等等
  3. 使用一些平时只有root权限才能执行的工具,例如tcpdump等等,例如tcpdump这些工具会将网卡设置为混杂模式,这是需要特殊权限的
  4. 创建设备
  5. 。。。

这些特殊权限的问题,就是capabilities

2.4.1. capabilities是什么?

在传统的linux内核里,内核的权限,是根据uid来决定的,内核只有两种权限:

  • privileged权限
  • unprivileged权限

euid=0的进程,会自动的被内核赋予privileged权限,而其他所有的euid != 0的进程,都是unprivileged权限

  • 拥有privileged权限的进程,能够通过内核所有的权限检查
  • 而unprivileged权限的进程,内核则会根据进程的 credentials (usually: effective UID, effective GID, and supplementary group list) 来决定进程对某项操作是否具有权限

unprivileged权限的进程内核为什么也要检查呢?其实是有的,因为内核支持通过文件系统提权, 具体可以看: https://linux.die.net/man/1/chmod

简单来说,就是内核支持给可执行文件设置set-user-ID或者set-group-ID标识,当一个普通用户,运行带有set-user-ID标识的进程时,内核会讲该进程的euid提升为set-user-ID,使之具备运行这个可执行文件的能力,从而达到普通用户也能使用需要privileged权限的工具

但是后来,内核认为,这种权限管理的方式粒度太大,而且不够灵活,于是从内核2.2开始,引入了capabilities机制

Starting with kernel 2.2, Linux divides the privileges traditionally associated with superuser into distinct units, known as capabilities, which can be independently enabled and disabled. Capabilities are a per-thread attribute

capabilities机制将原本属于euid=0的privileged权限,拆分成更细的粒度,让它变成了进程【注意:内核是没有线程的概念的,统一用进程描述】的属性

例如:

  1. CAP_MKNOD:创建设备的权限
  2. CAP_SYS_ADMIN: 可以执行mount & umount操作,可以执行ioctl等修改设备属性,等等
  3. CAP_SYS_BOOT: 拥有重启机器的能力
  4. 等等

如果你希望某个普通用户的进程(也就是unprivileged进程)也能执行某项特权操作,那么可以给这个进程设置相应的CAP_xxx标识

2.4.2. 新user namespace下的capabilities继承

从man7里我们可以了解到:

  1. 当新user namespace刚被创建出来的时候,这个进程的owner,在新的user namespace里是拥有这个user namespace里的所有的capabilities的
  2. 当进程使用unshare()或者setns()加入一个已经存在的user namespace时,这个进程同样会获得这个user namespace下的所有capabilities
  3. 换句话说,这个进程不再拥有父user namespace或者原先user namespace的任何capabilities,哪怕这个进程是root起的

另外还需要注意的是,当在新的user namespace里执行execve()系列函数时,会引起进程的capabilities的重新计算,计算方式可以参考capabilities(7)里的具体描述,这里不再解析,简单来说,如果进程的uid不是0的话,那么执行execve(2)之后,进程的所有capabilities都会被清空,也就是unprivileged权限进程了

总的来说,一个进程在user namespace里是否具有某个capability,主要是看如下几点:

  1. A process has a capability inside a user namespace if it is a member of that namespace and it has the capability in its effective capability set.  A process can gain capabilities in its effective capability set in various ways.  For example, it may execute a set-user-ID program or an executable with associated file capabilities.  In addition, a process may gain capabilities via the effect of clone(2), unshare(2), or setns(2), as already described.
  2. If a process has a capability in a user namespace, then it has that capability in all child (and further removed descendant) namespaces as well.
  3. When a user namespace is created, the kernel records the effective user ID of the creating process as being the "owner" of the namespace.  A process that resides in the parent of the user namespace and whose effective user ID matches the owner of the namespace has all capabilities in the namespace.  By virtue of the previous rule, this means that the process has all capabilities in all further removed descendant user namespaces as well.

这个地方跟之前说的uid/gid映射其实有个很重要的关系

你想一下,当一个普通用户刚创建了一个新的user namespace的时候,这个时候由于没有uid映射,进程在新的user namespace里其实是一个nobody账户。这个时候进程是没有 CAP_SETUID 能力的,那它怎么还怎么做uid映射呢?

内核其实考虑到这个情况了,所以当一个新的user namespace刚创建的时候,内核允许创建这个user namespace的进程设置一条只与进程owner相关的映射,但是要切记的是只允许一条

3. 容器的安全隔离

3.1. 理想方案: user namespace + capabilities

为什么需要考虑容器的安全隔离问题?这个和用户的需求是相关的,典型的比如:

  1. 需要root:例如安装软件
  2. 运行特权工具

安装软件为什么需要root?一方面是因为操作系统发行版里的安装软件必须是以root身份运行的,或者大部分工具对权限检查的方式简单粗暴:getuid() == 0,如果用户没有root账户,很多工具根本没办法运行

运行特权工具,例如tcpdump,chown等等

所以解决这种问题,user namespace + capabilities是非常理想的方案,user namespace提供容器一个fake root账户,同时通过capability机制允许fake root账户执行部分特权操作

3.2. user namespace与capabilities的兼容性问题

但是capabilities对user namespace的支持是不完整的,对于一些比较特殊的cap,即使user namespace下拥有某个cap,但是仍然过不了内核的检查

On the other hand, there are many privileged operations that affect resources that are not associated with any namespace type, for example, changing the system time (governed by CAP_SYS_TIME), loading a kernel module (governed by CAP_SYS_MODULE), and creating a device (governed by CAP_MKNOD). Only a process with privileges in the initial user namespace can perform such operations.

另外,还有一些相关的讨论是:

这样其实会有一个很严重的问题,那就是一旦开了user namespace之后,如果内核不支持,那么用户进程在容器内就永远执行不了特权操作,例如使用tcpdump工具。而在以前,即使是普通账户,如果我们给可执行文件加上+s标志位,也是能够运行特权工具的

3.3. docker的容器安全性方案

docker默认并没有使用user namespace,而是要显式的打开,那就是启动daemon进程的时候需要加上--userns-remap参数

并且,docker是不允许打开user namespace的情况下,还是用--privileged参数。这个主要还是到内核不支持

Leave a Comment