如何提高 docker 的安全性?
Docker容器多年来一直是开发人员工具箱的重要组成部分,通过 docker 以标准化的方式构建、分发和部署应用程序。随着容器技术的发展以及应用越来越广泛,其安全性问题也逐渐凸显出来。我们都知道容器其实就是运行在操作系统上的一个线程,通过 namespace 和 cgroup 技术与宿主机进行隔离。但这种隔离并非十分安全的,攻击者可以很容易利用参数的错误配置从容器内逃逸到宿主机上。
“容器”一词经常被误解,因为许多开发人员倾向于将隔离的概念与错误的安全感联系起来,认为这种技术本质上是安全的。
所谓的容器的安全,其实完全取决于以下几个方面:
- 配套基础设施的安全(例如操作系统、基础设施平台)
- 基础设施上的软件组件
- 运行时环境及相关配置
目前有很多有关容器的最佳实践都包含了提高容器的安全性措施。这些措施大都围绕着镜像构建、资源特权管理、文件系统、网络等方面。为了长期维护容器安全,- Clair 、Trivy、Docker 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)来运行程序的,这是一个十分危险的行为。所以需要通过以下两种方式来设定一个默认用户
- 在docker run时,指定一个容器中不存在的 user id
docker run -u 4000 <image>
- 在 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>
对持久化数据使用特定的文件系统
如果需要与主机文件系统或其他容器共享数据,则有两个选项:
- 创建挂载点,并对其使用限额进行限制。
--mount type=bind,o=size
- 为专用分区创建绑定卷
--mount type=volume
在这两种情况下,如果共享数据不需要由容器修改,则使用 --read-only
。
docker 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
上一篇: 黄金英雄传 DOS 版+秘诀
下一篇: 网络安全白帽子》第 1 章