在本系列关于 Docker 安全的第一篇文章中,我写道“容器不包含任何东西”。在第二篇文章中,我将介绍其原因以及我们正在采取的措施。
Docker、Red Hat 和开源社区正在共同努力,使 Docker 更加安全。当我查看安全容器时,我希望保护主机免受容器内进程的影响,并且我还希望保护容器彼此隔离。对于 Docker,我们正在使用分层安全方法,即“结合多种缓解安全控制措施来保护资源和数据的实践”。
基本上,我们希望设置尽可能多的安全屏障来防止突破。如果特权进程可以突破一种容器化机制,我们希望用下一种机制来阻止它们。对于 Docker,我们希望尽可能利用 Linux 的安全机制。
幸运的是,借助 Red Hat Enterprise Linux (RHEL) 7,我们获得了大量安全功能。
文件系统保护
只读挂载点
某些 Linux 内核文件系统必须在容器环境中挂载,否则进程将无法运行。幸运的是,这些文件系统中的大多数都可以挂载为“只读”。大多数应用程序永远不需要写入这些文件系统。
Docker 将这些文件系统作为“只读”挂载点挂载到容器中。
. /sys
. /proc/sys
. /proc/sysrq-trigger
. /proc/irq
. /proc/bus
通过将这些文件系统挂载为只读,特权容器进程无法写入它们。它们无法影响主机系统。当然,我们也阻止了特权容器进程将文件系统重新挂载为读/写的能力。我们阻止了在容器内挂载任何文件系统的能力。在讨论 capabilities 时,我将解释我们如何阻止挂载。
写时复制文件系统
Docker 使用 写时复制 文件系统。这意味着容器可以使用相同的文件系统镜像作为容器的基础。当容器将内容写入镜像时,它会被写入特定于容器的文件系统。这可以防止一个容器看到另一个容器的更改,即使它们写入了相同的文件系统镜像。同样重要的是,一个容器不能更改镜像内容来影响另一个容器中的进程。
Capabilities
Linux capabilities 在其主要页面上有很好的解释
为了执行权限检查,传统的 UNIX 实现区分了两种类型的进程:特权进程(有效用户 ID 为 0,称为超级用户或 root)和非特权进程(有效 UID 为非零)。特权进程绕过所有内核权限检查,而非特权进程则受制于基于进程凭据(通常是:有效 UID、有效 GID 和补充组列表)的完整权限检查。从内核 2.2 开始,Linux 将传统上与超级用户关联的特权划分为不同的单元,称为 capabilities,可以独立启用和禁用。Capabilities 是每个线程的属性。
删除 capabilities 可能会导致应用程序崩溃,这意味着我们在 Docker 中需要在功能、可用性和安全性之间取得平衡。以下是 Docker 当前使用的 capabilities 列表:chown、dac_override、fowner、kill、setgid、setuid、setpcap、net_bind_service、net_raw、sys_chroot、mknod、setfcap 和 audit_write。
关于默认情况下应允许或拒绝哪些 capabilities,一直存在争论。Docker 允许客户使用 Docker run 的命令行选项来操作默认列表。
已删除的 Capabilities
Docker 删除了以下几个 capabilities,包括:
CAP_SETPCAP | 修改进程 capabilities |
CAP_SYS_MODULE | 插入/删除内核模块 |
CAP_SYS_RAWIO | 修改内核内存 |
CAP_SYS_PACCT | 配置进程记帐 |
CAP_SYS_NICE | 修改进程优先级 |
CAP_SYS_RESOURCE | 覆盖资源限制 |
CAP_SYS_TIME | 修改系统时钟 |
CAP_SYS_TTY_CONFIG | 配置 tty 设备 |
CAP_AUDIT_WRITE | 写入审计日志 |
CAP_AUDIT_CONTROL | 配置审计子系统 |
CAP_MAC_OVERRIDE | 忽略内核 MAC 策略 |
CAP_MAC_ADMIN | 配置 MAC 配置 |
CAP_SYSLOG | 修改内核 printk 行为 |
CAP_NET_ADMIN | 配置网络 |
CAP_SYS_ADMIN | 全部捕获 |
让我们更仔细地看一下表中的最后几个。通过删除容器的 CAP_NET_ADMIN,容器进程无法修改系统网络,这意味着无法为网络设备分配 IP 地址、设置路由规则、修改 iptables。
所有网络都由 Docker 守护程序在容器启动之前设置。您可以从容器外部管理容器的网络接口,但不能从内部管理。
CAP_SYS_ADMIN 是一种特殊的 capability。我认为它是内核的全部捕获 capability。当内核工程师将新功能设计到内核中时,他们应该选择最适合该功能允许的 capability。或者,他们应该创建一个新的 capability。问题是,最初只有 32 个 capability 插槽可用。当有疑问时,内核工程师只会退回到使用 CAP_SYS_ADMIN。以下是根据 /usr/include/linux/capability CAP_SYS_ADMIN 允许的事项列表。
允许配置安全注意键 | 允许管理随机设备 |
允许检查和配置磁盘配额 | 允许设置域名 |
允许设置主机名 | 允许调用 bdflush() |
允许 mount() 和 umount(),设置新的 smb 连接 | 允许一些 autofs root ioctls |
允许 nfsservctl | 允许 VM86_REQUEST_IRQ |
允许在 alpha 上读取/写入 pci 配置 | 允许 mips 上的 irix_prctl (setstacksize) |
允许刷新 m68k 上的所有缓存 (sys_cacheflush) | 允许删除信号量 |
用于代替 CAP_CHOWN 来“chown” IPC 消息队列、信号量和共享内存 | 允许锁定/解锁共享内存段 |
允许打开/关闭交换 | 允许在套接字凭据传递上伪造 pid |
允许在块设备上设置预读和刷新缓冲区 | 允许在软盘驱动器中设置几何形状 |
允许在 xd 驱动器中打开/关闭 DMA | 允许管理 md 设备(主要是上述内容,但还有一些额外的 ioctls) |
允许访问 nvram 设备 | 允许管理 apm_bios、serial 和 bttv (TV) 设备 |
允许 isdn CAPI 支持驱动程序中的制造商命令 | 允许读取 pci 配置空间中非标准化的部分 |
允许 sbpcd 驱动程序上的 DDI 调试 ioctl | 允许设置串口 |
允许发送原始 qic-117 命令 | 允许在 SCSI 控制器上启用/禁用标记队列,并发送任意 SCSI 命令 |
允许在环回文件系统上设置加密密钥 | 允许设置区域回收策略 |
允许调整 ide 驱动程序 |
从容器中删除 CAP_SYS_ADMIN 的两个最重要的功能是阻止进程执行 mount 系统调用或修改命名空间。您不希望允许容器进程挂载随机文件系统或重新挂载只读文件系统。
--cap-add --cap-drop
Docker run 还具有一项功能,您可以在其中调整容器所需的 capabilities。这意味着您可以删除容器不需要的 capabilities。例如,如果您的容器不需要 setuid 和 setgid,您可以通过执行以下命令删除此访问权限
docker run --cap-drop setuid --cap-drop setgid -ti rhel7 /bin/sh
您甚至可以删除所有 capabilities 或添加所有 capabilities
docker run --cap-add all --cap-drop sys-admin -ti rhel7 /bin/sh
此命令将添加除 sys-admin 之外的所有 capabilities。
命名空间
Docker 为进程设置运行的一些命名空间也提供了一些安全性。
PID 命名空间
PID 命名空间隐藏了系统上运行的所有进程,但当前容器中运行的进程除外。如果您看不到其他进程,则攻击该进程会变得更加困难。您无法轻易地 strace 或 ptrace 它们。而且,杀死进程命名空间的 pid 1 将自动杀死容器内的所有进程,这意味着管理员可以轻松停止容器。
网络命名空间
网络命名空间可用于实现安全性。管理员可以使用路由规则和 iptables 设置容器的网络,以便容器内的进程只能使用某些网络。我可以想象人们设置了三个过滤容器
- 一个只允许在公共互联网上通信。
- 一个只允许在私有 Intranet 上通信。
- 一个连接到其他两个容器,在容器之间来回中继消息,但阻止不适当的内容。
cgroups
对系统的一种攻击可以描述为拒绝服务。这是指一个进程或一组进程使用系统上的所有资源,从而阻止其他进程执行。cgroups 可以通过控制任何 Docker 容器可以使用的资源量来缓解这种情况。例如,可以设置 CPU cgroup,以便管理员仍然可以登录到 Docker 容器试图占用 CPU 的系统并将其杀死。正在开发新的 cgroup,以帮助控制进程使用过多资源,例如打开的文件或进程数。Docker 将在这些 cgroup 可用时加以利用。
设备 cgroups
Docker 利用特殊的 cgroup,您可以指定容器内可以使用哪些设备节点。它阻止进程创建和使用可能用于攻击主机的设备节点。
设备节点允许进程更改内核的配置。控制哪些设备节点可用可以控制进程在主机系统上能够执行的操作。
默认情况下,在容器中创建以下设备节点。
/dev/console,/dev/null,/dev/zero,/dev/full,/dev/tty*,/dev/urandom,/dev/random,/dev/fuse
Docker 镜像也使用 nodev 挂载,这意味着即使设备节点已在镜像中预先创建,容器内的进程也无法使用它与内核对话。
注意:也可以通过删除 CAP_MKNOD capability 来阻止设备节点的创建。Docker 已选择不这样做,以便允许进程创建有限的设备节点集。在未来部分,我将提到 --opt 命令行选项,我希望使用它来删除此 capability。
AppArmor
Apparmor 可用于支持它的系统上的 Docker 容器。但我使用 RHEL 和 Fedora,它们不支持 AppArmor,因此您必须在其他地方研究此安全机制。(此外,您都知道我使用 SELinux。)
SELinux
首先,简单介绍一下 SELinux
- SELinux 是一个标记系统
- 每个进程都有一个标签
- 每个文件、目录和系统对象都有一个标签
- 策略规则控制标记进程和标记对象之间的访问
- 内核强制执行规则
SELinux 实施强制访问控制系统。这意味着对象的所有者对对象的访问没有控制权或酌情决定权。内核强制执行强制访问控制。我已经在SELinux 策略强制执行的可视化指南(以及后续的 SELinux 着色书)中描述了 SELinux 强制执行的工作原理。
我将使用该文章中的一些卡通来描述我们如何使用 SELinux 来控制允许 Docker 容器进程的访问。我们对 Docker 容器使用两种类型的 SELinux 强制执行。
类型强制执行
类型强制执行保护主机免受容器内进程的影响
我们用于运行 Docker 容器的默认类型是 svirt_lxc_net_t。所有容器进程都使用此类型运行。
容器内的所有内容都标记为 svirt_sandbox_file_t 类型。
svirt_lxc_net_t 允许管理标记为 svirt_sandbox_file_t 的任何内容。
svirt_lxc_net_t 还能够读取/执行主机上 /usr 下的大多数标签。
不允许使用 svirt_lxc_net_t 运行的进程打开/写入系统上的任何其他标签。不允许读取 /var、/root、/home 等中的任何默认标签。
基本上,我们希望允许进程读取/执行系统内容,但我们希望默认情况下不允许它使用系统上的任何“数据”,除非它在容器中。
问题
如果所有容器进程都使用 svirt_lxc_net_t 运行,并且所有内容都标记为 svirt_sandbox_file_t,那么容器进程是否可以攻击在其他容器中运行的进程以及其他容器拥有的内容?
这就是多类别安全强制执行发挥作用的地方,如下所述。
替代类型
请注意,我们在类型标签中使用了“net”。我们使用它来指示此类型可以使用完整的网络。我正在开发 Docker 的补丁,以允许用户指定用于容器的替代类型。例如,您可以指定类似的内容
docker run -ti --security-opt label:type:lxc_nonet_t rhel7 /bin/sh
然后,容器内的进程将不允许使用任何网络端口。同样,我们可以轻松编写一个 Apache 策略,该策略仅允许容器监听 Apache 端口,但不允许连接到任何端口。使用这种类型的策略,即使您的容器被破解,黑客控制了容器内的 apache 进程,您也可以防止您的容器变成垃圾邮件机器人。
多类别安全强制执行
多类别安全 (MCS) 保护一个容器免受其他容器的侵害
多类别安全基于多级安全 (MLS)。MCS 利用 SELinux 标签的最后一个组件 MLS 字段。MCS 强制执行可以保护容器彼此隔离。当容器启动时,Docker 守护程序会选择一个随机 MCS 标签,例如 s0:c1,c2,分配给容器。Docker 守护程序使用此 MCS 标签标记容器中的所有内容。当守护程序启动容器进程时,它会告诉内核使用相同的 MCS 标签标记进程。内核仅允许容器进程读取/写入其自己的内容,只要进程 MCS 标签与文件系统内容 MCS 标签匹配。内核阻止容器进程读取/写入标记有不同 MCS 标签的内容。
被黑的容器进程被阻止攻击不同的容器。Docker 守护程序负责保证没有容器使用相同的 MCS 标签。这是我制作的一个视频,展示了如果 OpenShift 容器能够获得系统 root 权限会发生什么。 相同的基本策略用于限制 Docker 容器。
正如我在上面提到的,我正在开发 Docker 的补丁,以允许指定不同的 SELinux 内容。我将允许管理员指定容器的标签。
docker run --ti --rm --label-opt level:TopSecret rhel7 /bin/sh
这将允许人们在多级安全 (MLS) 环境中开始运行容器,这对于需要 MLS 的环境可能很有用。
SELinux 注意事项
文件系统支持
SELinux 目前仅适用于设备映射器后端。SELinux 不适用于 BTFS。BTRFS 尚不支持上下文挂载标记,这会阻止 SELinux 在容器通过 mount 命令启动时重新标记所有内容。内核工程师正在努力修复此问题,以及 Overlayfs(如果它被合并到容器中)。
卷挂载
由于类型强制执行仅允许容器进程在容器中读取/写入 svirt_sandbox_file_t,因此卷挂载可能会成为问题。卷挂载只是将目录绑定挂载到容器中,因此目录的标签不会更改。为了允许容器进程读取/写入内容,您需要将类型标签更改为 svirt_sandbox_file_t。
卷挂载 /var/lib/myapp
chcon -Rt svirt_sandbox_file_t /var/lib/myapp
我为 docker 编写了一个补丁,但尚未合并到上游,以自动设置这些标签。使用该补丁,您的 docker 将自动将卷重新标记为私有标签“Z”或共享标签“z”。
docker run -v /var/lib/myapp:/var/lib/myapp:Z ...
docker run -v /var/lib/myapp:/var/lib/myapp:z ...
希望这能尽快合并。
底线
我们添加了许多安全机制,使 Docker 容器比在裸机上运行应用程序更安全,但您仍然需要像我在关于此主题的第一篇文章中谈到的那样,保持良好的安全实践。
- 仅运行来自可信来源的应用程序
- 在企业级质量的主机上运行应用程序
- 定期安装更新
- 尽快降低权限
- 尽可能以非 root 用户身份运行
- 监视您的日志
- setenforce 1
我的下一篇关于 Docker 安全的文章将介绍我们接下来正在努力进一步保护 Docker 容器安全的工作。
1 条评论