本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

下面说下容器安全的模块。

容器的安全很大程度由容器的架构特性决定的:比如容器与宿主机共享Linux 内核,通过Namespace来做资源的隔离,通过 shim/runC 的方式来启动等。

这些容器架构特性,在选择使用容器之后,作为容器的用户,其实没有多少能力去对架构层面做安全上的改动了。
你可能会说用Kata Container、gVisor 就是安全“容器”了。不过,Kata 或者 gVisor 只是兼容了容器接口标准,而内部的实现完全是另外的技术了。

对于使用容器的用户,在运行容器的时候,在安全方面可以:

  • 赋予容器合理的capabilities
  • 在容器中以非root用户来运行程序

一、问题再现

缺省docker run 的方式启动容器后,容器中很多操作都是不允许的,即使是以root用户来运行程序也不行。

例子

# docker run --name iptables -it registry/iptables:v1 bash
[root@0b88d6486149 /]# iptables -L
iptables v1.8.4 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)
 
[root@0b88d6486149 /]# id
uid=0(root) gid=0(root) groups=0(root)

看上面的返回结果,提示没有权限,容器中都已经是root了。

查阅资料,容器启动的时候有个 “privileged” 参数,我们用了这个参数后,iptables 就启动成功了。

# docker stop iptables;docker rm iptables
iptables
iptables
# docker run --name iptables --privileged -it registry/iptables:v1 bash
[root@44168f4b9b24 /]# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
 
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
 
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

使用 privileged 是否合理。代码 里看到,使用该参数就会获取所有的 capabilities

            if ec.Privileged {
                        p.Capabilities = caps.GetAllCapabilities()
            }

二、基本概念

2.1、Linux capabilities

Linux capabilities 官方手册的描述。

在Linux capabilities 出现前,进程的权限分为两类:

  • 特权用户的进程,进程的有效用户ID 是0,简单来说就是root 用户
  • 非特权用户的进程,进程的有效用户ID 是非0 ,就是非root用户

特权用户进程可以执行Linux 系统上的所有操作
而非特权用户进程在执行某些操作的时候就会被内核限制

从kernel 2.2 开始,Linux 开始把特权做了更详细的划分,划分出来的每个单元就被称为 capability

所有的 capabilities 都在 Linux capabilities 的手册列出来了。

对于任意一个进程,在做任意一个特权操作的时候,都需要有这个特权操作对应的capability。

比如说,运行 iptables 命令,对应的进程需要有 CAP_NET_ADMIN 这个capability ,如果要mount 一个文件系统,那么对应的进程需要有 CAP_SYS_ADMIN 这个capability

CAP_SYS_ADMIN 这个capability 里允许了大量的特权操作,包括文件系统,交换空间,还有对各种设备的操作,以及系统调试相关的调用等。

在普通的Linux 节点上,非root 用户启动的进程缺省没有任何 Linux capability ,而root 用户启动的进程缺省包含了所有的 Linux capability。

==

做个实验,对于root用户启动的进程,把CAP_NET_ADMIN 这个capability 移除,看看是否有权限运行 iptables 命令。

可以使用 capsh 命令来移动某个 capability

# sudo /usr/sbin/capsh --keep=1 --user=root   --drop=cap_net_admin  --   -c './iptables -L;sleep 100'
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
 
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
 
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
iptables: Permission denied (you must be root).

如上图,可以看到root用户,如果把CAP_NET_ADMIN 移除了,那么执行 iptables 的时候就会看到"Permission denied (you must be root)."的提示信息。

同时我们可以通过/proc/ 文件系统找到对应进程的status,这样就能确认进程中的CAP_NET_ADMIN 是否已经被移除了。

# ps -ef | grep sleep
root     22603 22275  0 19:44 pts/1    00:00:00 sudo /usr/sbin/capsh --keep=1 --user=root --drop=cap_net_admin -- -c ./iptables -L;sleep 100
root     22604 22603  0 19:44 pts/1    00:00:00 /bin/bash -c ./iptables -L;sleep 100
 
# cat /proc/22604/status | grep Cap
CapInh:            0000000000000000
CapPrm:          0000003fffffefff
CapEff:             0000003fffffefff
CapBnd:          0000003fffffefff
CapAmb:         0000000000000000

对于当前进程,直接影响某个特权操作是否可以被执行的参数,是"CapEff",也就是"Effective capability sets",这是一个 bitmap,每一个 bit 代表一项 capability 是否被打开。

在 Linux 内核capability.h里把 CAP_NET_ADMIN 的值定义成 12,所以我们可以看到"CapEff"的值是"0000003fffffefff",第 4 个数值是 16 进制的"e",而不是 f。

这表示 CAP_NET_ADMIN 对应的第 12-bit 没有被置位了(0xefff = 0xffff & (~(1 << 12))),所以这个进程也就没有执行 iptables 命令的权限了。

对于进程 status 中其他几个 capabilities 相关的参数,它们还需要和应用程序文件属性中的 capabilities 协同工作,这样才能得到新启动的进程最终的 capabilities 参数的值。

在这里插入图片描述
如果我们要启动一个程序,在Linux里的过程就是先通过fork()来创建出一个子进程,然后调用 execve() 系统调用来读取文件系统里的程序文件,把程序文件加载到进程的代码段中开始运行。

就像图中所描绘的那样,这个新运行的进程里相关的capabilities 参数的值,由它的父进程以及程序文件中的capabilities 参数值计算得来的。

文件中可以设置capabilities 参数值,并且这个值会影响到最后运行它的进程

比如我们把iptables的应用程序加上 CAP_NET_ADMIN 的capability ,那么即使非root用户也可以执行iptables了。


$ id
uid=1000(centos) gid=1000(centos) groups=1000(centos),10(wheel)
$ sudo setcap cap_net_admin+ep ./iptables
$ getcap ./iptables
./iptables = cap_net_admin+ep
$./iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
 
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
…

Linux capabilities 就是把Linux root 用户原来所有的特权做了细化,可以更加细粒度的给进程赋予不同的权限。

三、解决问题

privileged的容器就是允许容器中的进程可以执行所有的特权操作

容器缺省启动的时候,系统最多只允许15个capabilities runC spec 文档中的 security 部分。

也可以查看容器中 init 进程 status 里的cap 参数,看一下容器缺省的capabilities。

# docker run --name iptables -it registry/iptables:v1 bash
[root@e54694652a42 /]# cat /proc/1/status  |grep Cap
CapInh:            00000000a80425fb
CapPrm:          00000000a80425fb
CapEff:              00000000a80425fb
CapBnd:          00000000a80425fb
CapAmb:         0000000000000000

直接使用 privileged 参数把所有的capabilities 都给容器是很危险的事情。
容器中的进程有了 CAP_SYS_ADMIN 的特权之后,这些进程就可以在容器里直接访问磁盘设备,直接可以读取或者修改宿主机上的所有文件了。

所以在容器平台上是基本不允许把容器直接设置为privileged的,我们需要根据容器中进程需要的最少特权来赋予 capabilities。

开头的问题,iptables 需要CAP_NET_ADMIN 这个capabilities ,我们只要在运行Docker 的时候,给这个容器多加一个 NET_ADMIN 参数就可以了。

--cap-add NET_ADMIN

# docker run --name iptables --cap-add NET_ADMIN -it registry/iptables:v1 bash
[root@cfedf124dcf1 /]# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
 
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
 
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

四、重点小结

Linux capabilities 就是把Linux root 用户原来所有的特权做了细化,可以更加细粒度的给进程赋予不同的权限。

对于Linux 中每一个特权都有一个对应的capability,对于一个capability,有的对应一个权限操作,有的对应很多个特权操作。

每个Linux 进程有5个capabilities 集合参数,其中Effctive 集合里的capabilities 决定了当前进程可以做哪些特权操作。
而其他的集合参数会和应用程序文件的capabilities 集合参数来一起决定新启动程序的capabilities 集合参数。

对于容器的root用户,缺省只赋予了15个capabilities。
如果发现容器中的进程的权限不够,就需要分析它需要最小的capabilities集合,而不是直接赋予容器 privileged。

因为"privileged"包含了所有的 Linux capabilities, 这样"privileged"就可以轻易获取宿主机上的所有资源,这会对宿主机的安全产生威胁。所以,我们要根据容器中进程需要的最少特权来赋予 capabilities。

五、评论

1、
getcap $(which ping)
setcap -r $(which ping)

顺便举个之前使用过的例子:普通用户默认没有 tcpdump 抓包权限,可添加 net_raw、net_admin caps:
sudo setcap cap_net_raw,cap_net_admin+ep $(which tcpdump)

2、
getcap /usr/bin/ping 查看ping进程当前cap
setcap cap_net_admin,cap_net_raw+p /usr/bin/ping 设置ping进程cap

getcap setap capsh 等命令的解释的博客,很牛逼的博客
https://www.cnblogs.com/ryanyangcs/p/11798292.html

3、
问题:
selinux是不是实际上就是限制cap权限的操作

回答:
selinux 是通过对object, 例如进程\文件, 打上label来控制这些object的相互作用。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐