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

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

那么对于使用容器的用户,在运行容器的时候,在安全方面可以做些什么呢?我们主要可以从这两个方面来考虑:

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

为什么是这两点呢?我通过两讲的内容和你讨论一下,这一讲我们先来看容器的 capabilities 的问题。 

问题再现


 刚刚使用容器的同学,往往会发现用缺省 docker run的方式启动容器后,在容器里很多操作都是不允许的,即使是以 root 用户来运行程序也不行。

我们用下面的例子来重现一下这个问题。我们先运行make image 做个容器镜像,然后运行下面的脚本:

# 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)

在这里,我们想在容器中运行 iptables 这个命令,来查看一下防火墙的规则,但是执行命令之后,你会发现结果输出中给出了"Permission denied (you must be root)"的错误提示,这个提示要求我们用 root 用户来运行。

不过在容器中,我们现在已经是以 root 用户来运行了,么为什么还是不可以运行"iptables"这条命令呢?

你肯定会想到,是不是容器中又做了别的权限限制?如果你去查一下资料,就会看到启动容器有一个"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"参数来解决问题,是不是一个合理的方法呢?用它会有什么问题吗?

要回答这些问题,我们先来了解一下"privileged"是什么意思。从 Docker 的代码里,我们可以看到,如果配置了 privileged 的参数的话,就会获取所有的 capabilities,那什么是 capabilities 呢?

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

基本概念 Linux capabilities


要了解 Linux capabilities 的定义,我们可以先查看一下"Linux Programmer's Manual"中关于Linux capabilities的描述。

在 Linux capabilities 出现前,进程的权限可以简单分为两类,第一类是特权用户的进程(进程的有效用户 ID 是 0,简单来说,你可以认为它就是 root 用户的进程),第二类是非特权用户的进程(进程的有效用户 ID 是非 0,可以理解为非 root 用户进程)。

特权用户进程可以执行 Linux 系统上的所有操作,而非特权用户在执行某些操作的时候就会被内核限制执行。其实这个概念,也是我们通常对 Linux 中 root 用户与非 root 用户的理解。

从 kernel 2.2 开始,Linux 把特权用户所有的这些“特权”做了更详细的划分,这样被划分出来的每个单元就被称为 capability。 

所有的 capabilities 都在Linux capabilities的手册列出来了,你也可以在内核的文件capability.h中看到所有 capabilities 的定义。

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

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

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

我们可以做个试验,对于 root 用户启动的进程,如果把 CAP_NET_ADMIN 这个 capability 移除,看看它是否还可以运行 iptables。

在这里我们要用到capsh这个工具,对这个工具不熟悉的同学可以查看超链接。接下来,我们就用 capsh 执行下面的这个命令:

# 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

运行上面的命令查看 /proc//status 里 Linux capabilities 的相关参数之后,我们可以发现,输出结果中包含 5 个 Cap 参数。

这里我给你解释一下, 对于当前进程,直接影响某个特权操作是否可以被执行的参数,是"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 参数的值。

我们看下面的图,结合这张图看后面的讲解:

Logo

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

更多推荐