如何提高docker的安全性?

202 阅读6分钟

Docker容器多年来一直是开发人员工具箱的重要组成部分,通过 docker 以标准化的方式构建、分发和部署应用程序。随着容器技术的发展以及应用越来越广泛,其安全性问题也逐渐凸显出来。我们都知道容器其实就是运行在操作系统上的一个线程,通过 namespace 和 cgroup 技术与宿主机进行隔离。但这种隔离并非十分安全的,攻击者可以很容易利用参数的错误配置从容器内逃逸到宿主机上。

“容器”一词经常被误解,因为许多开发人员倾向于将隔离的概念与错误的安全感联系起来,认为这种技术本质上是安全的。

所谓的容器的安全,其实完全取决于以下几个方面:

  • 配套基础设施的安全(例如操作系统、基础设施平台)
  • 基础设施上的软件组件
  • 运行时环境及相关配置

目前有很多有关容器的最佳实践都包含了提高容器的安全性措施。这些措施大都围绕着镜像构建、资源特权管理、文件系统、网络等方面。为了长期维护容器安全,- ClairTrivyDocker Bench for Security 这些安全扫描工具不可或缺。

以下这些最佳实践的安全性措施仅限于对 Docker 的使用,并未包含任何 K8s 及 Docker Compose的理念与操作。但是对使用 K8s 或 Docker Compose 也有参考意义。

镜像构建

基础镜像的选择

基础镜像尽量选用可信的镜像,比如docker hub 的官方镜像。如果需要使用基础的发行版,则推荐使用 Alpine Linux 的基础镜像,因为他轻量级以及阉割部分功能,可以保证受攻击面比较小。当然使用alpine版本的镜像也有缺点,由于缺少linux部分依赖,则需要手动补全、安装软件的各种依赖,也有一定的使用门槛。

当然也可以使用 Google 所引入的 Distroless 镜像作为基础镜像。其最大的特点是仅仅包含程序以及其依赖项,并不包含标准 Linux 发行版中的包管理器、shell或其他任何程序。

这里有一个实验,分别基于 Apline 和 Distroless 作为基础镜像,来构建简单的 Hello World 的 Springboot 应用镜像。此处参考文章 使用哪个容器镜像——Distroless 还是 Alpine? 最终结论是:

镜像大小— 使用 Alpine 基础镜像编译的镜像为93.5 MB,而 distroless 镜像为139 MB。因此,与 distroless 镜像相比,Alpine 镜像更轻巧。

漏洞数量——Alpine 镜像共有:216 个漏洞(未知:0,低:106,中:79,高:27,关键:4)而 Distroless Image 共有:50 个漏洞(未知:0,低) : 30, 中: 6, 高: 9, 关键: 5)

使用非特权用户

在默认的情况下,容器内的进程是以 root(id=0)来运行程序的,这是一个十分危险的行为。所以需要通过以下两种方式来设定一个默认用户

  1. 在docker run时,指定一个容器中不存在的 user id docker run -u 4000 <image>
  2. 在 Dockerfile 中创建一个默认用户
FROM <base image>

RUN addgroup -S appgroup \
 && adduser -S appuser -G appgroup
 
USER appuser

... <rest of Dockerfile> ...

这种方案则需要关注基础镜像中使用何种工具来创建用户了

使用单独的用户ID命名空间

默认情况下,Docker守护进程使用主机的用户ID所在的命名空间。因此,当容器内权限提升后,则会以root方式来访问宿主机主机以及其他容器。 为了降低此风险,应该将主机和Docker守护程序配置为使用带有--userns remap选项的单独名称空间。

当心环境配置

在 Dockerfile中,ENV指令中不应包含任何敏感信息。例如:

ENV $VAR
RUN unset $VAR

即使这样做,$VAR其实仍在镜像中,且很容易被读取。

为了增加读取限制,应该在构建的每一层中,在引入环境变量后将其销毁:

RUN export ADMIN_USER="admin" \
    && do something \   
    && unset ADMIN_USER

不要将 docker.sock 暴露在容器中

这个不用多说了,由于Docker的 C/S 架构,docker.sock 是 Docker API的主要入口。如果放开这个入口,相当于把自家的大门完全敞开了。

资源特权管理

特权是十分危险的,容器永远不应该以特权运行,否则容器中的进程将会拥有主机上的root权限及功能。docker 的创建应该增加--security-opt=no-new-privileges 参数来限制这种特权。

另一方面,Docker 利用linux的capabilities机制来进行细粒度的权限控制访问。容器会使用默认的一组capabilities,但是大部分我们基本都不会用到。一种建议是将所有的 capabilities 删除,仅根据程序需求来单独添加。例如运行 web 服务器的容器, 其实仅需要 NET_BIND_SERVICE 的能力,用于绑定服务器低于1024端口的权限(一般web服务器需要使用 80 端口)

第三,不要共享主机文件系统中的敏感目录:

  • root (/),
  • device (/dev)
  • process (/proc)
  • virtual (/sys) mount points. 如有需要,则谨慎的选择目录的权限设置

使用 Control Group 限制对资源的访问

控制组是用于控制每个容器对CPU、内存、磁盘I/O的访问的机制。默认情况下,容器与独立的cgroup相关联,但如果存在选项--cgroup parent,则会使主机资源面临DoS攻击的风险,因为这允许主机和容器之间共享资源。

文件系统

使用只读权限

当容器是临时启用,且是无状态服务时,则应该将挂载的文件系统限制为只读。 docker run --read-only <image>

对非持久性数据使用临时目录

docker run --read-only --tmpfs /tmp:rw ,noexec,nosuid <image>

对持久化数据使用特定的文件系统

如果需要与主机文件系统或其他容器共享数据,则有两个选项:

  1. 创建挂载点,并对其使用限额进行限制。--mount type=bind,o=size
  2. 为专用分区创建绑定卷 --mount type=volume

在这两种情况下,如果共享数据不需要由容器修改,则使用 --read-onlydocker run -v <volume-name>:/path/in/container:ro <image>
或是 docker run --mount source=<volume-name>,destination=/path/in/container,readonly <image>

网络

不要使用Docker的默认桥接docker0

docker0 是Docker用于将主机网络与容器网络分离的网桥。 当一个容器创建时,默认情况下Docker会将其连接到docker0网络。因此,所有容器都连接到docker0,并能够相互通信。这也将存在安全隐患。

此刻应该通过--bridge=none禁用所有容器的默认连接,并使用以下命令为每个连接创建专用网络: docker network create <network_name> docker run --network=<network_name>

比如传统的web服务,反向代理与web服务使用同一个网络, web服务器与数据库连接则使用另外一个网络。

不要共享主机的网络命名空间

容器网络不应共享宿主机的网络命名空间,即不应该使用--network host

译自 How to improve your Docker containers security