6.Docker镜像与容器安全最佳实践

2022-09-28 19:36:28 浏览数 (1)

[TOC]

0x00 前言简述

描述: 在企业中信息系统安全与业务是同样重要, 随着传统运维方式向着容器化运维方式的转变,当下企业里通常都会采用Docker来进行容器化部署和承载业务, 由于运维人员或者开发人员对容器安全的关注较少, 只是简单认为容器是有隔离和限制的, 就算是容器被黑客攻击了, 也单单是容器内部受到影响, 而对宿主的 Linux 系统和网络都不会产生太大影响。其实不然Docker容器安全也是重中之重, 它关乎着应用与数据安全,其中也关乎着宿主机的安全。

所以在本章之中,我将讲述如何更安全配置使用Docker容器环境,优化Docker镜像的安全构建过程,以使我们能够在最短构建时间内构建最小、最安全的满足生产需求的Docker镜像。

温馨提示: 本文中的技巧与示例应该可以提高您的Docker容器安全知识, 并可以改善所使用的Docker镜像质量,希望读者从中有所收获。

Docker 安全问题公告: https://docs.docker.com/security/

1.容器安全概述

在正式学习Docker容器安全之前,先解释一下本章节会涉及的相关概念,它可以帮助你扫除概念障碍,以及更好的理解容器安全应该从那几个方面入手。

  • Docker 宿主机: 安装Docker服务的操作系统主机。
  • Docker 服务: 即Docker提供的相关功能以及在宿主机中的Docker进程,Docker进程是可以直接与Linux内核进行通信的。
  • Docekr 镜像: 通过Dockerfile文件构建得到的产物。
  • Docker 容器: 通过镜像创建运行多个Docker容器,即业务是运行在容器之中,注意Docker容器是运行于Docker服务之上。

Docker 宿主机安全 描述: 宿主机作为Docker服务运行的基础环境, 其重要性是无可置疑的,一个安全的基础环境是保证Docker服务安全的基础设施,所以在安装使用docker前我们需要针对其宿主机操作系统进行一系列的加固配置,具体如何进行宿主机的安全加固设置,可以参考我前面所写的Windows与Linux系统加固脚本。

Docker 服务安全 描述: Docker 服务的安全同样在容器安全中占据着重要地位,如果一旦Docker服务由于脆弱性配置被攻击者控制,将会影响所有运行在该Docker服务下的所有容器,将不能保证业务与数据的安全,攻击者完全可以通过Docker Server提供的REST API接口进行管理容器或者查看容器内某些重要的配置文件。

Docekr 镜像安全 描述: Docker 镜像安全也是在容器安全中占有一席之地,如果一旦镜像系统或者服务中存在可以被攻击者利用的漏洞,在使用该镜像创建并运行容器后便可能反弹shell进行内网穿透,从而对容器中运行的业务、业务数据产生影响,并且还可能会影响Docker宿主机的安全。所以说一个安全的Docker镜像也是保证容器安全的重要一环,即我们需在构建镜像时选择较小的操作系统并只安装业务需要的服务与软件即可,减少攻击面从而增加攻击难度。

Docekr 容器安全 描述: Docker 容器作为承载业务的地方,是运维或开发人员接触最多的对象之一,它也是容器安全里的重要一环,如果在创建容器时未对某些功能做相应的限制,一旦docker容器中承载的业务产生漏洞或者包含的动态调用的脚本程序出现问题,都有可能严重危害到容器或者宿主机的安全。所以说为了保证docker容器安全我们不但要从docker安全入手,更要从业务安全入手。

从上面四大方面可以看出,容器安全并不是简单在某一环做出相应的安全配置就可以的,我们需要考虑四个主要方面,一是内核的内在安全性及其对命名空间和 cgroup 的支持,二是Docker 守护进程本身的攻击面,三是容器配置文件中的漏洞,四是内核的“强化”安全特性以及它们如何与容器交互。所以我们必须从基础操作系统环境、容器服务、容器镜像以及业务与运维开发人员分别入手,才能提高容器的安全性,减少被攻击的可能性。

2.容器安全机制

描述: Docker 容器与 LXC 容器非常相似,并且具有相似的安全特性,当您使用 docker run创建或启动容器时,Docker 服务为了防止黑客在控制容器后能够对宿主机进行攻击,提供了三个主要的隔离机制,其分别是Namespace 机制、Capabilities 机制和 CGroups 机制

通过隔离机制能起到对容器一定的保护,但并不是绝对的,如果攻击对宿主机产生了影响,就说明入侵者已经突破了Docker服务的保护,这就是容器安全中常说的Docker容器逃逸。

内核能力机制

描述: 通过前面的学习我们知道Docker服务进程是直接与Linux 内核进行通信的,而Linux内核从从2.6.24版本起提供了一个强大的特性,就是可以提供细粒度的权限访问控制操作能力,既可以用在进程上,也可以作用在文件上,使用内核能力机制对加强 Docker 容器的安全有很多好处;

默认情况下,Docker 启动的容器被严格限制只允许使用内核的一部分能力。并且Docker采用白名单机制,禁用必需功能之外的其它权限

例如,一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限,并不需要 root 权限。那么它只需要被授权 net_bind_service 能力即可。

为了加强安全,容器可以禁用一些没必要的权限。

  • 完全禁止任何 mount 操作;
  • 禁止直接访问本地主机的套接字;
  • 禁止访问一些文件系统的操作,比如创建新的设备、修改文件属性等;
  • 禁止模块加载。

这样就算攻击者在容器中取得了 root 权限,也不能获得本地主机的较高权限,能进行的破坏也有限。

Namespace 机制 描述: Namespace 即名称空间,是Linux内核提供的一种标签机制,Linux内核会针对不同Namespace之间的进程做隔离,避免不同的进程之间互相产生影响,所以Docker服务会为每一个 Docker容器创建一个单独的 Namespace 空间。 由于不同容器之间、容器和系统之间都是不同名称的Namespace,所以在一个容器中运行的进程无法看到运行在另一个容器或主机系统中的进程,并且每个容器还拥有自己的网络堆栈,这意味着一个容器无法获得对另一个容器的套接字或接口的特权访问,但通常Docker主机上的所有容器都是默认位于桥接接口docker0上,除非在创建容器之初指定了其他网络模式。

虽然 Namespace 实现了容器和宿主机环境的”伪隔离”, 但是由于输入、输出、硬件设备的需要,容器仍然可以修改宿主机中部分文件,所以我们又需要一种可以限制对象操作的权限的机制,即下面要说的Capabilities机制。

Capabilities 机制 描述: Capabilities 提供了更细粒度的授权机制,它定义了主体对象能够进行的某一类操作。例如当一个容器的Web服务需要绑定 到80端口,但是80端口的绑定是需要ROOT权限的。而Docker为了防止 ROOT 权限滥用会通过 Capabilities 机制,给予该容器Web 服务对象 net_bind_service 的权限(其允许绑定到小于 1024 的端口)。

同样地Docker服务对容器中的ROOT权限用户添加了很多默认的限制,比如:拒绝所有的挂载操作、拒绝部分文件的操作(如修改文件所有者等)、拒绝内核模块加载;

虽然 Capabilities 可以最大程度解决容器安全问题, 但Capabilities对容器可进行操作的限制程度是难以把控的,如果设置过松会导致 Docker 容器影响宿主机系统,让 Docker 隔离失效。而如果设置过为严格的话会让容器以及容器内的服务功能受限,导致Docker容器无法正常运行。

所以在默认情况下,Docker 会采用白名单机制(白名单列表你可以在 Docker 源码中查看)进行限制,即只允许 Docker 容器拥有几个默认的能力。那有了白名单限制,即使攻击者成功拿到了容器中的 ROOT 权限,能够对宿主机造成的影响也相对较小,所以“Docker 逃逸”并不是一件不容易的事。

CGroups 机制 描述: CGroups 提供了资源限制的作用能力,Docker 服务可以利用 CGroups 机制来实现对容器中内存、CPU处理和磁盘IO份额等的限制,减少Docker容器由于业务或者攻击从而过多占用宿主机资源,对宿主机以及其他容器产生影响。

比如,通过下面的命令就可以限制 Docker 容器只使用 2 个 CPU 和 200MB 的内存来运行。

代码语言:javascript复制
docker run -it --cpus=2 --memory="200m" debian:latest /bin/bash

虽然 CGroups 可以解决给每一个容器弹性地分配 CPU 、内存或者其他资源。同样地该限制既不能过松,也不能过为严格,如果设置过松会导致某一 Docker容器耗尽宿主机资源。而如果设置过严又会使得容器内的服务得不到足够的资源支持,可能无法运行。所以此时需要我们自己根据业务的压测结果来进行配置,没有默认的安全机制可以辅助。

服务端防护机制

Docker 服务的运行目前需要 root 权限,因此其安全性十分关键,由于运行一个容器或应用程序的核心是通过 Docker 服务端。

Docker 允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制; 例如:恶意用户启动容器的时候将主机的根目录/映射到容器的 /host 目录中,那么容器理论上就可以对主机的文件系统进行任意修改了 因此当提供容器创建服务时(例如通过一个 web 服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器。

Docker 的安全特性:

  • 首先,确保只有可信的用户才可以访问 Docker 服务(理论上由于攻击层出不穷)。
  • 其次, 在容器内不使用 root 权限来运行进程的话。
  • 确保只有可信的网络或 VPN,或证书保护机制(例如受保护的 stunnel 和 ssl 认证)下的访问可以进行。
  • 将容器的 root 用户映射到本地主机上的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题;
  • 允许 Docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作,例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。
辅助安全机制

描述: 除此之外,我们还可以利用一些现有的安全机制来增强使用 Docker 的安全性,例如 TOMOYO, AppArmor, SELinux, GRSEC 等。 Docker 当前默认只启用了能力机制,用户可以采用多种方案来加强 Docker 主机的安全,例如:

  • 在内核中启用 GRSEC 和 PAX,这将增加很多编译和运行时的安全检查;通过地址随机化避免恶意探测等。并且启用该特性不需要 Docker 进行任何配置。
  • 使用一些有增强安全特性的容器模板,比如带 AppArmor 的模板和 Redhat 带 SELinux 策略的模板。这些模板提供了额外的安全特性。
  • 用户可以自定义访问控制机制来定制安全策略。 跟其它添加到 Docker 容器的第三方工具一样(比如网络拓扑和文件系统共享),有很多类似的机制,在不改变 Docker 内核情况下就可以加固现有的容器。

默认情况下,如果运行容器内以非特权用户身份运行进程时,容器通常是相对安全的,但是您仍然可以通过启用 AppArmor、SELinux、GRSEC 或其他适当的组件服务系统来添加额外的安全层,来更进一步来保证容器的安全。

3.容器安全风险

描述: 本小节将针对容器化环境中,您有可能会遇到安全风险进行一一罗列,其主要分为如下几方面。

Docker 容器安全常见问题

  • 宿主机问题 例如,宿主机上其他服务漏洞导致的网络穿透代理访问,内网探测、Docker守护进程Socket文件读取。
  • 自身漏洞问题 例如,代码执行、权限提升、信息泄漏、runC。
  • 架构缺陷与安全机制未配置问题 例如,Namespaces 导致的:容器之间的局域网攻击、共享root、未隔离的文件系统、默认放通所有。 例如,CGroups 导致的: DDoS攻击耗尽资源。
  • 镜像源问题 例如,恶意镜像、存在漏洞的镜像、容器逃逸。
  • 生态问题 例如,Containerd 相关漏洞、Kubernetes相关漏洞。

Docker 容器安全防护基线

  • 内核级别的:Namespaces、Cgroup、SElinux
  • 主机级别的:服务最小化、禁止挂载宿主机敏感目录、挂载目录权限设置为644
  • 网络级别的:禁止映射特权端口、通过iptable设定规则并禁止容器之间的网络流量
  • 镜像级别的:创建本地镜像服务器、使用可信镜像、使用镜像扫描、合理管理镜像标签
  • 容器级别的:容器以单一主进程方式运行、禁止运行SSH等高危服务、以只读方式挂载根目录系统

0x01 Docker 镜像安全最佳实践

描述: Docker 安全管理要趁早,下面分别列举了几个诀窍和指南,确保为测试与生产环境提供更安全和更高质量的 Docker 镜像。

1.选用最小化基础镜像

描述: 运维人员在编写项目的 Dockerfile 时,经常使用一个通用的 Docker 容器镜像作为基础例如From Node,而Node 镜像实际上是以一个完整安装的 Debian Stretch 发行版为基础,这意味着构建得到的项目容器镜像将包含一个完整的操作系统。所以如果该项目不需要任何通用的系统库或者系统工具应用,最好不要使用完整的操作系统作为基础镜像。

Synx 发布的《开源安全报告-2019[3]》指出,Docker Hub 上流行的很多容器镜像,都用到了包含大量已知安全漏洞的基础镜像。例如执行 docker pull node ,下载并使用 Node 镜像,相当于在应用中引入了一个包含 580 个已知漏洞的操作系统。

WeiyiGeek.Top ten most popular docker images each contain at least 30 vulnerabilities

安全实践: 选用最小化基础镜像,即只包含项目确实需要的系统工具和库的镜像,就能最小化系统的攻击面,确保所用操作系统是安全的。

2.设定最小权限的 USER 运行容器

描述: 如果 Dockerfile 中没有指定 USER ,Docker 默认将会以超级用户 root 的身份运行容器,容器所属的命名空间(namespace)因此映射为 root 所有,这意味着容器有可能获取 Docker 宿主机的超级管理权限。不仅如此以 root 用户身份运行容器,还扩大了攻击面,如果容器应用中存在安全漏洞,很容易造成权限提升。

在实践中一般不需要容器拥有 root 权限。为了尽量降低安全威胁,创建专门的用户和用户组,在 Dockerfile 中使用 USER 指定用户,确保以最小权限的用户身份运行容器应用。

安全实践:

1) 如果基础镜像中不包含专门的用户,那么就在 Dockerfile 中直接创建。

代码语言:javascript复制
FROM ubuntu
RUN mkdir /app && 
    groupadd -r weiyigeek && 
    useradd -r -s /bin/false -g weiyigeek weiyigeek
WORKDIR /app
COPY . /app
RUN chown -R weiyigeek:weiyigeek /app
USER weiyigeek
CMD node index.js
# 关键命令解释
# - 创建一个系统用户( -r 选项),没有密码、没有主目录且没有 shell,并将该用户添加到前面(使用 groupadd )创建的用户组;

2) 如果你使用的是 Node.js 和 alpine 镜像,已经包含了一个用户 node,直接使用即可:

代码语言:javascript复制
FROM node:10-alpine 
RUN mkdir /app
COPY . /app
RUN chown -R node:node /app
USER node
CMD ["node", "index.js"]

3.签名和校验镜像,防范中间人攻击

描述: Docker 镜像的认证颇具挑战性。在生产环境使用这些镜像运行我们的代码,意味着我们对这些镜像的极大信任。因此必须保证我们拉取的容器镜像确实是发布者发布的镜像,没有被任何人篡改。发生镜像篡改,有可能是因为 Docker 客户端和镜像中心之间的中间人攻击,或者是发布者的身份被人盗用并在镜像中心发布了恶意镜像。

安全实践:

1) 校验 Docker 镜像: Docker 默认直接拉取容器镜像,不会校验镜像的来源和发布者。这意味着你有可能使用来源和发布者不明的任何镜像。无论采用何种策略,最佳实践都是先校验容器镜像,通过验证后再拉取镜像。

代码语言:javascript复制
# 1.为了体验镜像校验功能我们暂时开启 Docker Content Trust ,可以在系统环境变量中进行执行命令并且可以加入到/etc/profile文件中。
export DOCKER_CONTENT_TRUST=1
# 2.现在尝试拉取一个没有签名的容器镜像——请求会被拒绝不会拉取镜像。
$ docker pull kacha886/buysbox
  Using default tag: latest   # 默认latest标记是没有签名的
  Error: remote trust data does not exist for docker.io/kacha886/buysbox: notary.docker.io does not have trust data for docker.io/kacha886/buysbox
$ docker pull busybox:1.33.1    # 本地不存在busybox 1.33.1的信任数据(没有签名信息)
  No valid trust data for 1.33.1

# 3.我们在设置容器校验的环境中可通过 --disable-content-trust 标志关闭内容信任后在标记镜像上运行单独的操作即可正常拉取、构建、上传非签名镜像。
docker pull --disable-content-trust busybox:1.33.1
docker build --disable-content-trust -t weiyigeek/buysbox:notrust .
docker login -u weiyigeek
docker push --disable-content-trust weiyigeek/busybox:1.33.1

# 4.推送一个有签名的可信的镜像到仓库中
docker push weiyigeek/buysbox:trust
  # The push refers to a repository [docker.io/weiyigeek/trust] (len: 1)
  # 9a61b6b1315e: Image already exists
  # 902b87aaaec9: Image already exists
  # latest: digest: sha256:d02adacee0ac7a5be140adb94fa1dae64f4e71a68696e7f8e7cbf9db8dd49418 size: 3220
  # Signing and pushing trust metadata
  # You are about to create a new root signing key passphrase. This passphrase
  # will be used to protect the most sensitive key in your signing system. Please
  # choose a long, complex passphrase and be careful to keep the password and the
  # key file itself secure and backed up. It is highly recommended that you use a
  # password manager to generate the passphrase and keep it safe. There will be no
  # way to recover this key. You can find the key in your config directory.
  # Enter passphrase for new root key with id a1d96fb:
  # Repeat passphrase for new root key with id a1d96fb:
  # Enter passphrase for new repository key with id docker.io/weiyigeek/trust (3a932f1):
  # Repeat passphrase for new repository key with id docker.io/weiyigeek/trust (3a932f1):
  # Finished initializing "docker.io/weiyigeek/trust"

2) 签名 Docker 镜像: Docker 支持镜像签名,提供了额外一层的保护,使用 Docker Notary 签名镜像Notary 会检验镜像的签名,如果签名不合法,它会阻止运行该镜像。并在选择基础镜像时优先使用 Docker 认证的镜像,即这些镜像来自经过 Docker Hub 检查和选择的可信提供者,一定不要使用无法检验来源和发布者的容器镜像。

如果开启了 Docker Content Trust ,构建 Docker 镜像的同时也会对镜像签名,例如推送可信标记镜像到仓库流程。

代码语言:javascript复制
- 提示需要创建一个新的根密钥
- 请求根密钥的密码
- 在 ~/.docker/trust 目录中生成一个根密钥
- 请求仓库密钥的密码
- 在 ~/.docker/trust 目录中生成一个仓库密钥

示例演示:

代码语言:javascript复制
# 1.基础镜像拉取
$ docker pull --disable-content-trust busybox:1.33.1
  # 1.33.1: Pulling from library/busybox
  # b71f96345d44: Pull complete
  # Digest: sha256:930490f97e5b921535c153e0e7110d251134cc4b72bbb8133c6a5065cc68580d
  # Status: Downloaded newer image for busybox:1.33.1
  # docker.io/library/busybox:1.33.1

# 2.构建自定义镜像
$ tee dockertrust/dockerfile <<'EOF'
FROM busybox:1.33.1
MAINTAINER weiyigeek master@weiyigeek.top
CMD ["echo","Welcome to Visited www.weiyigeek.top"]
EOF
$ docker build --disable-content-trust -t weiyigeek/buysbox:1.33.1-trust .

# 3.运行自定义镜像的容器
$ docker run --disable-content-trust weiyigeek/buysbox:1.33.1-trust
# Welcome to Visited www.weiyigeek.top

# 4.登录到docker hub仓库之中(https://hub.docker.com)
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you dont have a Docker ID, head over to https://hub.docker.com to create one.
Username: weiyigeek
Password: xxx

# 5.上传可信赖的镜像到docker hub仓库之中。
$ docker push weiyigeek/buysbox:1.33.1-trust
The push refers to repository [docker.io/weiyigeek/buysbox]
5b8c72934dfc: Mounted from library/busybox
1.33.1-trust: digest: sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662 size: 527
Signing and pushing trust metadata
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new root key with ID 7fcbe50:
Repeat passphrase for new root key with ID 7fcbe50:
Enter passphrase for new repository key with ID 0d40411:
Repeat passphrase for new repository key with ID 0d40411:
Finished initializing "docker.io/weiyigeek/buysbox"
Successfully signed docker.io/weiyigeek/buysbox:1.33.1-trust

# 6.查看生成的根私有密钥和仓库密钥
$ ls ~/.docker/trust/private/
0d4041172f5d7286c5199510ad25b795a65bfc774d6e01ce2948c5e217df553f.key  7fcbe50705b0e48526d35b8f928bc52054e899ff08edb44f4fb023fb50979cf1.key

# 7.删除可信赖的镜像后并通过远程仓库拉取可信赖镜像
$ docker rmi -f weiyigeek/buysbox:1.33.1-trust
$ docker pull docker.io/weiyigeek/buysbox:1.33.1-trust
Pull (1 of 1): weiyigeek/buysbox:1.33.1-trust@sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662  # 关键点拉取时可以看到内容信任后的特征
sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662: Pulling from weiyigeek/buysbox
Digest: sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662
Status: Downloaded newer image for weiyigeek/buysbox@sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662
Tagging weiyigeek/buysbox@sha256:99959645871654685ef84dbc4f3cb541a4f5332505752fbb89f0af80e6c84662 as weiyigeek/buysbox:1.33.1-trust
docker.io/weiyigeek/buysbox:1.33.1-trust

Tips: 在镜像存储库可以包含同时具有已签名标签和未签名标签的镜像,例如 Mongo 镜像仓库latest 标签是未签名的而 3.1.6 标签是签名的。 Tips: 首次调用使用内容信任的操作时会创建密钥集,而密钥集由以下几类密钥组成, 1)作为镜像标记的内容信任的根的脱机密钥 2)签名标签的仓库或标签密钥 3)服务器管理的密钥,如时间戳密钥,为仓库提供最新的安全保证 Tips: 如果是第一次签名Docker 会为当前用户生成一个私钥,保存在 ~/docker/trust后续所有的镜像都会使用这个私钥签名,丢失根密钥非常难以恢复纠正这种损失需要 Docker 支持人员的干预来重置仓库状态,所以最好是将应该将根密钥备份到安全的地方。

4.检测、修正和监控开源漏洞

描述: 在指定容器的基础镜像时,同时也引入了该镜像包含的操作系统及系统库有可能存在的所有安全风险。前面我们提到过最好选用能够正常运行应用代码的最小化镜像,这有助于减少攻击面,因为限制了可能的安全漏洞数量。不过这么做并没有对镜像进行安全审计,也不能防范将来发现的新漏洞。

最佳安全实践,我们应该尽可能早地在开发过程中集成安全性,我们越早开始进行安全检查,对组织来说就越便宜,更有效,所以我们要在构建镜像时进行采用辅助扫描工具和安全漏洞库进行集成扫描,例如Snyk、Trivy等工具,其中使用较多的是Snyk引擎。

  • Snyk : 它是一个开发者优先选择的云原生安全工具,该工具可以扫描并监控您的项目构建的基础镜像是否存在安全漏洞,其主要功能是查找并自动修复开源漏洞、实时查找并修复应用程序代码中的漏洞、查找并修复容器镜像和 Kubernetes 应用程序中的漏洞以及查找并修复 Terraform 和 Kubernetes 代码中的不安全配置 ,并且Docker和Snyk建立了合作伙伴关系,以提供容器漏洞扫描以及将Snyk扫描引擎直接集成到docker-cli和Docker Desktop客户端中,但是在此之前我们必须需要将我们构建好的基础环镜像推送到镜像仓库中,才能进行漏洞的扫描,而现在我们可以在上传到仓库前进行镜像漏洞检测。 项目地址: https://github.com/snyk/snyk
  • Trivy: 它是是一款简单而全面的扫描仪,用于检查容器映像、文件系统和Git存储库中的漏洞,以及配置问题,它已经被集成到Harbor项目之中为镜像提供安全扫描服务。其主要功能是可以检测操作系统软件包(Alpine、RHEL、CentOS等)和特定语言软件包(Bundler、Composer、npm、Thread等)的漏洞,此外Trivy会将基础设施扫描为代码(IaC)文件,如Terraform、Dockerfile和Kubernetes,以检测可能导致部署面临攻击风险的潜在配置问题,其优点是安装简单,开箱即用。项目地址: https://github.com/aquasecurity/trivy/

温馨提示: 基于扫描效率的考虑,出现了server-client模式,在初次扫描时,server会下载所需的漏洞数据库,并在后台持续获取最新的数据库。

WeiyiGeek.Trivy架构

1.Snyk扫描引擎安装与使用

代码语言:javascript复制
步骤01.参考Snyk在github项目地址,我们可以快速进行了解和使用。
# docker已经集成安装snyk引擎,所以已安装docker的用户无法再次安装snyk。
docker scan

注意: Docker Scan依赖于对第三方提供商Snyk的访问,每月只免费提供10次扫描的限制,如超过此限额使用则可以执行docker scan --login命令进行登陆或者注册Snyk。

步骤02.在本地使用docker scan命令的好处是,我们可以在推送任何代码之前在本地捕获安全漏洞。
# 1.使用前需要先登陆docker.io
$ docker login

# 2.snyk 扫描引擎版本查看
$ docker scan --version
Version:    v0.12.0
Git commit: 1074dd0
Provider:   Snyk (1.790.0 (standalone))

步骤03.构建镜像以及扫描构建的镜像存在的安全问题,扫描结果如下图所示。
# 1.构建并标记镜像
docker build -t weiyigeek/go-webserver:v2.0 .

# 2.扫描镜像
docker scan weiyigeek/go-webserver:v2.0

# Mongo镜像进行扫描
docker scan mongo:latest

Tips: Snyk 和 Docker 宣布达成合作关系,以帮助开发人员安全地构建和使用容器以及开源,Docker在2.3.6.0或更高版本中包括了一个名为的新命令docker scan。运行docker scan命令时将根据Snyk安全引擎扫描本地镜像,从而使您可以安全查看本地Dockerfile和本地镜像。

WeiyiGeek.Snyk扫描

2.Trivy 扫描引擎安装与使用

代码语言:javascript复制
- 步骤 01.快速进行 Trivy 镜像安全扫描仪安装,主要有两种方式一种是常规的二进制文件方式,第二种则是通过带有该工具的镜像。
# 方式1
# 1.从Github 的releases中拉取 Trivy 最新的二进制执行文件。
wget https://github.com/aquasecurity/trivy/releases/download/v0.23.0/trivy_0.23.0_Linux-64bit.tar.gz -P /tmp

# 2.解压并设置快捷执行方式。
tar -zxvf /tmp/trivy_0.23.0_Linux-64bit.tar.gz -C /usr/local/bin

# 3.执行权限赋予给trivy二进制文件
chmod  x /usr/local/bin/trivy

# 4.查看trivy版本
trivy --version
Version: 0.23.0

# 方式2.拉取带有Trivy 执行环境的镜像
docker pull aquasec/trivy:0.23.0
docker pull ghcr.io/aquasecurity/trivy:0.23.0
docker pull public.ecr.aws/aquasecurity/trivy:0.23.0

- 步骤 02.我们可以执行扫描镜像命令,注意首次扫描镜像会自动更新下载漏洞库,反馈的扫描结果,如下图所示、
# 首次扫描可能需要更新漏洞库
$ trivy image weiyigeek/go-webserver:v1.0
2022-02-24T11:58:30.219 0800    INFO    Need to update DB
2022-02-24T11:58:30.219 0800    INFO    Downloading DB...

$ trivy image weiyigeek/go-webserver:v2.0


- 步骤 03.除此之外,我们可以利用其扫描本地目录中的Dockerfile镜像配置文件和远程仓库中的Dockerfile文件扫描,它会针对我们编写的Dockerfile进行检查并给出合理的处理建议,例如。
# 此处,利用多阶段构建的Dockerfile文件进行演示trivy针对构建文件内容扫描。
$ ls
Dockerfile  webserver.go

# 扫描本地配置文件
$ trivy config Dockerfile

# 扫描git远程仓库配置文件
$ trivy repo https://github.com/weiyigeek/trivy-ci-test

# 扫描本地文件系统以查找特定于语言的依赖项和配置文件
$ trivy fs /path/to/project

WeiyiGeek.trivy扫描

温馨提示: 除了synk和Trivy,其实还有多种工具可以执行镜像安全扫描,例如由Anchore Inc.开发的Anchore和 由Quay开发的Clair等

5.容器镜像中禁止包含机密信息

描述: 运维人员在构建包含应用的容器镜像时可能需要用到一些机密信息,例如从私有仓库拉取代码所需的 SSH 私钥,或者安全私有软件包所需的令牌。如果 Dockerfile 中包含复制机密信息的命令,构建镜像时,这行命令对应的中间容器会被缓存,导致机密数据也被缓存,有可能造成机密信息泄漏。因此像令牌和密钥这样的机密信息必须保存在 Dockerfile 之外,所以为了避免机密信息的泄露我们可以采用使用多阶段构建、使用 Docker 的 secret 管理功能、避免无意中复制机密信息等三种方式进行联合使用。

安全实践

1) 使用多阶段构建: 利用 Docker 的多阶段构建功能,用一个中间镜像层获取和管理机密信息,然后清除中间镜像,这样在应用镜像构建阶段不涉及敏感数据。

代码语言:javascript复制
# 例子使用代码将机密信息添加到中间层
FROM: ubuntu as intermediate
WORKDIR /app
COPY secret/key /tmp/
RUN scp -i /tmp/key build@weiyigeek.top/files .

FROM ubuntu
WORKDIR /app
COPY --from intermediate /app .

2) 使用 Docker 的 secret 管理功能:加载敏感信息文件且不会缓存这些信息

代码语言:javascript复制
# syntax = docker/dockerfile:1.0-experimental
FROM alpine
# shows secret from default secret location
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecre
# shows secret from custom secret location
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

3) 避免无意中复制机密信息: 在构建镜像时通常都会采用COPY . .的指令将整个构建上下文文件夹复制到 Docker 镜像,但也有可能把敏感文件也复制进去了。所以如果文件夹中有敏感文件,要么先移除这些文件,要么将这些文件包含在.dockerignore中复制时会忽略这些文件:

代码语言:javascript复制
cat .dockerignore <<'EOF'
private.key
appsetting.config
appsettings.json
EOF

6.设定镜像的标签,保证镜像的不可更改性

描述: 每个 Docker 镜像可以有多个标签(tag),代表该镜像的不同变体。最常见的标签是 latest 表示这是该镜像的最新版本。镜像标签是可更改的,也就是说镜像的作者可以多次发布相同标签的镜像。 因此,即使你的 Dockerfile 明确指定了使用的基础镜像及其标签,这次镜像构建和下次镜像构建仍然可能用到了不同的基础镜像。

安全实践:

  • 1) 优先选用最详细的镜像标签。例如,镜像有8、:8.0.1 和 :8.0.1-alpine 等标签,选择最后这个,因为它提供了最详细的信息,不要建议使用像 latest 这样过于泛泛的标签。
  • 2) 发布者有可能删除镜像的某个标签。应该提前把该镜像复制到私有镜像中心或者公有镜像中心的私人账户下。
  • 3) 使用比签名更具体的 SHA256 引用指明要使用的镜像。好处是能保证每次拉取都是相同内容的镜像,缺点是如果镜像发生改变需要及时的更新SHA256 引用(散列值)。

7.镜像构建更加安全、快速、精简

描述: 在实际的生产场景中我们常常需要对基础的镜像根据实际需求编写 dockerfile 进行 build 重构建, 除了前面提到过的最小的基础镜像外,还需要注意下面的几个方面囊括了LABEL、COPY与ADD等指令

安全实践:

(1) 使用 LABEL 指定镜像元数据: 镜像元数据有助于用户更好地理解和使用该镜像并且除了镜像的维护者信息,添加其他你认为重要的元数据,包括提交对象的散列值、相关构建的链接、质量状态(通过所有测试了吗?)、源代码链接、SECURITY.TXT 文件的位置等。

代码语言:javascript复制
MAINTAINER 
LABEL maintainer="test@weiyigeek.top"
LABEL securitytxt="https://www.example.com/.well-known/security.txt"

(2) 尽量使用COPY而非ADD指令: 从而为了避免可能导致的安全问题请记住 COPY 和 ADD 的不同

  • COPY - 将本地文件或者目录(递归)复制到容器镜像中的目标目录,复制来源和目标都必须明确指定。
  • ADD - 1) 如果复制来源是本地压缩文件,ADD 将把该文件解压缩到目标目录; 2) ADD 也可以将远程 URL 指定的文件下载到目标目录。

Q: 使用COPY指令相比较于ADD指令的优点及安全性?

使用 ADD 从远程 URL 下载文件,存在中间人攻击的风险,文件内容有可能因此被篡改。必须确保远程 URL 必须是安全的 TLS 链接,校验远程 URL 的来源和身份。译者注:实际上,官方文档并不鼓励使用 ADD 添加远程文件。 如果复制的是本地压缩文件,ADD 自动将它解压缩到目标目录,这有可能触发 zip 炸弹或者 zip 任意文件覆盖漏洞。 相比较而言使用 COPY 复制文件或目录,会创建一个缓存的中间镜像层,优化镜像构建的速度。

(3) 善用RUN指令: 在dockerfile之中我们常常能看见RUN指令的身影, 所以为了减少镜像构建时的Layer的数量, 我们可以通过将所有RUN命令合并成为一条命令。

代码语言:javascript复制
# 示例1:添加用户并加入到用户组里并创建一个app目录设置weiyigeek为所属者我们只需要一个RUN指令
RUN groupadd weiyigeek && 
    useradd weiyigeek -r -s /bin/false -g weiyigeek &&
    mkdir /app && 
    chown weiyigeek:weiyigeek -R /app

# 示例2
# Bad, Creates 4 layers
RUN yum --disablerepo=* --enablerepo="epel"
RUN yum update
RUN yum install -y httpd
RUN yum clean all -y

# Good, creates only 1 layer
RUN yum --disablerepo=* --enablerepo="epel" && 
  yum update && 
  yum install -y httpd && 
  yum clean all -y

(4) 缓存以加快构建速度: 镜像的构建时间大都花在系统软件包和应用程序依赖包的下载和安装。但是,这些通常不会经常变更,因此推荐进行缓存。

代码语言:javascript复制
export DOCKER_BUILDKIT=1
# 使用命令--mount选项RUN来选择缓存目录
FROM python:3.8 
COPY pom.xml ./pom.xml                   # Java
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline -B             # Java

FROM openjdk:15.0.1
COPY requirements.txt ./requirements.txt # Python
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt   # Python
# 镜像构建
docker build -t python:3.8-test .

(5) 使用多阶段构建小而安全的镜像: 使用 Dockerfile 构建应用容器镜像时,会生成很多只是构建时需要的镜像层,包括编译时所需的开发工具和库,运行单元测试所需的依赖、临时文件、机密信息等等, 如果保留这些镜像层,不仅会增加镜像的大小,影响镜像下载速度,而且会因为安装更多软件包而面临更大的攻击危险。所以在实践环境中我们常常将软件构建阶段所需的镜像存放到内部的镜像仓库之中,并不会将它用来作为运行应用的环境。

Docker 为我们提供了多阶段构建的功能,允许在构建过程中使用多个临时镜像,只保留最后一个镜像,因此用户可以得到两个镜像:

  • 第一个镜像——非常大的镜像,包含了构建应用和运行测试所需的所有依赖;
  • 第二个镜像——非常小的镜像,只包含运行应用所需的极少数依赖。
代码语言:javascript复制
# 第一阶段
FROM golang as builder
WORKDIR /go/src/app
COPY . .
# Static build is required so that we can safely use 'scratch' base image
RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"'

# 第二阶段
FROM scratch
COPY --from=builder /go/bin/app /app
ENTRYPOINT ["/app"]

8.使用静态分析工具,Dockerfile编写指导

描述: 使用静态分析工具,能够避免常见的错误,建立工程师自动遵循的最佳实践指南,在集成开发环境(IDE)中使用 hadolint 更好。

例如,使用hadolint工具分析 Dockerfile 并列出不符合最佳实践规则的地方,或者在 VS Code 安装的 hadolint 扩展后,编写 Dockerfile 时边写边检查既快又好。

代码语言:javascript复制
docker run --rm -i hadolint/hadolint < Dockerfile

WeiyiGeek.

0x02 Docker 容器安全最佳实践

1.主机安全配置

1.1 更新docker到最新版本

描述: Docker频繁发布更新,旧版本可能存在安全漏洞,应及时更新。 加固说明: 通过及时了解Docker更新,Docker中的漏洞可以得到修复。攻击者可能会尝试获得访问权限或提升权限时利用已知的漏洞。不安装常规的Docker更新可能会让现有的Docker受到攻击。可能会导致提升权限,未经授权的访问或其他安全漏洞。所以需要跟踪新版本并根据需要进行更新。 判断方法: 和最新版本进行比对,查看是否为最新。 检测加固: 检查docker版本是否为最新docker version,跟踪Docker发布并根据需要进行更新。

代码语言:javascript复制
# 1.检查docker版本是否为最新,和最新版本进行比对,查看是否为最新。
$ docker version

# 2.跟踪Docker发布并根据需要进行更新,例如此处在ubuntu上更新docker。
$ apt update && apt-cache madison docker-ce
docker-ce | 5:20.10.12~3-0~ubuntu-focal | https://download.docker.com/linux/ubuntu focal/stable amd64 Packages
$ apt install docker-ce docker-ce-cli

操作影响: 有些使用Docker的第三方产品可能依赖较老版本的Docker。

默认值: 不适用

备注: Docker频频曝出漏洞问题,应密切关注容器安全相关漏洞

1.2 为容器创建一个单独的分区

描述: 默认情况下,所有Docker容器、镜像及数据和元数据都存储在 /var/lib/docker 目录下。 加固说明: /var/lib/docker作为docker默认目录,其存储所有Docker相关文件,包括镜像文件。该目录写满时,会导致Docker、甚至主机可能无法使用。因此,建议为Docker创建一个单独的分区。 判断方法: 应该返回/var/lib/docker挂载点的分区详细信息。 检测加固: 新安装docker时为/var/lib/docker挂载点创建一个单独的分区,对于先前安装的系统可使用LVM创建分区。

代码语言:javascript复制
# 1.查看docker数据目录
# $ df -h | grep docker
$ docker info -f '{{.DockerRootDir}}'
/app/docker
$ docker info | grep "Docker Root Dir:"
Docker Root Dir: /app/docker

# 2.配置 docker data 数据挂载点,修改后重载systemd守护进行以及重新docker服务。
$ vim /etc/docker/daemon.json
# 在json格式的 {} 中加入如下字段及内容。
"data-root": "/app/docker",
$ systemctl daemon-reload && systemctl restart

操作影响: None

默认值: /var/lib/docker将根据可用性挂载在/或/var分区下。

1.3 只有受信任的用户才能控制docker守护进程

描述: Docker守护进程绑定到unix socks需要root权限运行。对于添加到docker组的用户为提供了完整的root访问权限。 加固说明: Docker允许在Docker主机和容器之间共享目录,而不会限制容器的访问权限。这意味着可以启动容器并将主机上的根目录映射到容器。容器将能够不受任何限制地更改的主机文件系统。 判断方法: 判断是否必须要加入docker组的用户 检测加固: 检查docker用户组里的用户.从’docker’组中删除任何不受信任的用户,不要在主机上创建敏感目录到容器卷的映射。

代码语言:javascript复制
# 建议docker组中不包含root或者其他高权限用户。
# 建立 docker 组: $ sudo groupadd docker
$ sudo usermod -aG docker $USER   # 将当前低权限用户加入 docker 组

# 检查系统中用户组里的用户是否必须要加入docker组的用户
$ grep "docker" /etc/group
docker:x:998:root,app

操作影响: 作为普通用户构建和执行容器的权限将受到限制

默认值: 不适用

1.4 审计docker守护进程

描述:审计所有活动的Docker守护进程 加固说明:除了审核常规的Linux文件系统和系统调用外,还要审Docker守护进程。Docker守护进程以root特权运行。因此有必要审核其活动和使用情况。 判断方法:应该列出Docker守护进程的规则 检测加固:验证是否存在Docker守护进程的审计规则,并为Docker守护进程添加审计规则

代码语言:javascript复制
# 安装审计进程
sudo apt install auditd

# Docker守护进程的审核规则
auditctl -l | grep /usr/bin/docker

# 采用命令向 /etc/audit/audit.rules 添加审计规则
auditctl -w /usr/bin/docker -k docker
# 重新启动审计守护进程
systemctl restart auditd.service

操作影响:审计生成相当大的日志文件,确保定期归档它们,另外建议创建一个单独的审计分区以避免写满根文件系统。

默认值:默认安装后,Docker守护进程没有审计

1.5 审计docker相关的文件和目录

描述: 在条件允许的情况,审计docker相关的文件和目录,例如 docker.service、/etc/default/docker、/etc/docker docker.socket、daemon.json、/var/lib/docker 加固说明 除了正常的Linux文件系统和系统调用审核外,还可以审核所有与Docker相关的文件和目录。Docker守护进程以root权限运行。 判断方法: 如果以上文件存在,验证是否存在与之对应的审核规则,应该根据其位置列出docker相关的规则。 检测加固方法: 添加审计规则

代码语言:javascript复制
# 在 /etc/audit/audit.rules 文件中添加以下行,然后重新启动审计守护进程
$ touch /etc/audit/rules.d/docker-audit.rules
$ cat > /etc/audit/rules.d/docker-audit.rules <<'EOF'
-w /usr/bin/docker -k docker
-w /usr/lib/systemd/system/docker.service -k docker 
-w /usr/lib/systemd/system/docker.socket -k docker
-w /usr/bin/docker-containerd -k docker
-w /usr/bin/docker-runc -k docker
-w /etc/docker -k docker
-w /etc/docker/daemon.json -k docker
EOF

操作影响: 审核会生成相当大的日志文件。确保定期归档。另外需要创建一个单独的审计分区,以避免填写根文件系统。

默认值: 默认情况下,Docker相关的文件和目录不会被审计,文件 docker.service 可能在系统上不可用


2.docker守护进程配置

2.1 限制默认网桥上容器之间的网络流量

描述: 默认情况下网桥上同一主机上的容器之间允许所有网络通信。如果不需要所有网络通信,建议限制容器间通信。将需要通信的特定容器链接在一起。或者创建自定义网络,并只加入需要与该自定义网络通信的容器。 加固说明: 每个容器都有可能读取同一主机上容器网络上的所有数据包。这可能会导致意外和不必要的信息泄露给其他容器。因此,限制默认网桥上的容器间通信。 判断方法: 它应该返回默认网桥的com.docker.network.bridge.enable_icc:false。 检测加固: 在守护进程模式下运行docker并传递–icc=false作为参数或创建自定义网络,注意–icc参数仅适用于默认网桥,如果使用自定义网络,则应采用分段网络的方法。

代码语言:javascript复制
# 运行以下命令并确认默认网桥已被配置为限制集装箱间通信。
docker network ls -q | xargs docker network inspect --format '{{.Name}}: {{.Options}}'

# 设置不同容器之间是不允许网络互通的。

操作影响: 默认网桥上的容器间通信将被禁用。如果需要在同一主机上的容器之间进行通信,则需要使用容器链接来明确定义它,或者必须定义自定义网络。

默认值: 默认情况下,默认网桥上允许所有容器间通信。

2.2 设置日志级别为info

描述: 将Docker守护进程日志级别设置为info。 加固说明: 设置适当的日志级别,配置Docker守护进程以记录需要查看的事件。info及以上的基准日志级别将捕获除调试日志之外的所有日志。若无必须,不应该在’debug’日志级别运行Docker守护进程 检测加固:

代码语言:javascript复制
# 确保--log-level参数不存在或存在,然后将其设置 info。
ps -ef | grep dockerd
grep "log-level" | /etc/docker/daemon.json

# 运行Docker守护进程参数如下。
Dockerd --log-level=info

操作影响: None.

默认值: 默认情况下,Docker守护进程设置为info的日志级别。

2.3 允许 docker 更改iptables

描述: iptables用于建立、维护和检查Linux内核中的IP包过滤规则表。允许Docker守护进程更改iptables 加固说明: Docker会根据用户为容器选择网络选项的方式自动对iptables进行必要的更改。建议让Docker自动更改iptables,以避免可能妨碍容器与外界通信的网络配置错误。 检测加固: 不要使用’–iptables=false’参数运行Docker守护进程。

代码语言:javascript复制
# 确保'--iptables'参数不存在或不设置为'false'
ps -ef | grep dockerd

操作影响: Docker守护进程需要在启动之前启用iptables规则。在Docker守护进程操作期间任何重新启动iptables都可能导致丢失docker创建的规则。使用iptables-persistent持久iptables规则可以帮助减轻这种操作影响。

默认值: 默认情况下,’iptables’设置为’true’。

2.4 不使用不安全的镜像仓库

描述: Docker在默认情况下私有仓库被认为是相对安全的,所以我们需要保证私有镜像仓库的安全。 加固说明: 一个安全的镜像仓库建议使用TLS,在 /etc/docker/certs.d/<registry-name>/目录下,将镜像仓库的CA证书副本放置在Docker主机上。不安全的镜像仓库是没有有效的镜像仓库证书或不使用TLS的镜像仓库。不应该在生产环境中使用任何不安全的镜像仓库。不安全的镜像仓库中的镜像可能会被篡改,从而导致生产系统可能受到损害。 检测加固:

代码语言:javascript复制
# 使用镜像扫描工具检测, 验证本地或者远程镜像仓库是否存在不安全的基础镜像。
# 查看镜像所属以及构建操作
docker images 
docker history

操作影响: None.

默认值: 默认情况下,Docker假定所有的本地镜像仓库都是安全的。

备注: Hub仓库中中各基础发行版官方的镜像也可能是不安全的,我们需要用镜像扫描工具进行扫描验证。

2.5 建议不使用aufs存储驱动程序

描述:不要使用’aufs’作为Docker实例的存储驱动 加固说明:aufs代码太烂没能加入Linux内核主线,在Docker中只是保留了历史遗留支持, 检测加固:

代码语言:javascript复制
# 执行以下命令并验证aufs不被用作存储驱动,此时命令结果不应该返回aufs。
docker info | grep -e StorageDriver:s*aufss*$

# 在启动dockerd不要设置--storage-driveraufs参数,不要刻意的使用'aufs'作为存储驱动。
dockerd --storage-driveraufs

操作影响:aufs’是允许容器共享可执行文件和共享库内存的存储驱动程序。如果使用相同的程序或库运行数千个容器可以选用。

默认值:默认情况下,在大多数平台上使用overlay2和devicemapper作为Docker存储驱动程序。默认存储驱动程序可能因操作系统而异。应该首选操作系统最佳支持的存储驱动程序。

备注:在许多使用最新Linux内核的发行版中,’aufs’不再被支持。

2.6 docker守护进程配置TLS身份认证

描述:可以让Docker守护进程监听特定的IP和端口以及除默认Unix套接字以外的任何其他Unix套接字。配置TLS身份验证以限制通过IP和端口访问Docker守护进程。 加固说明:默认情况下,Docker守护进程绑定到非联网的Unix套接字,并以root权限运行。如果将默认的docker守护进程更改为绑定到TCP端口或任何其他Unix套接字,那么任何有权访问该端口或套接字的人都可以完全访问Docker守护进程,进而可以访问主机系统。因此,不应该将Docker守护进程绑定到另一个IP/端口或Unix套接字。如果必须通过网络套接字暴露Docker守护进程,建议为守护进程和 Docker Swarm API配置TLS身份验证。 检测加固: 按照Docker文档或其他参考中提到的步骤进行操作.

代码语言:javascript复制
# 执行如下命令,确保存在以下参数:'--tlsverify'·'--tlscacert'·'--tlscert'·'--tlskey'
ps -ef | grep dockerd

操作影响: 需要管理Docker守护进程和Docker客户端的证书和密钥。

默认值: 默认情况下,未配置TLS认证

备注

2.7 配置合适的 ulimit 资源控制

描述: 根据业务环境设置默认的ulimit选项 加固说明:ulimit提供对shell可用资源的控制。设置系统资源控制可以防止资源耗尽带来的问题,如fork炸弹。有时候合法的用户和进程也可能过度使用系统资源,导致系统资源耗尽。为Docker守护进程设置默认ulimit将强制执行所有容器的ulimit。不需要单独为每个容器设置ulimit。但默认的ulimit可能在容器运行时被覆盖。因此,要控制系统资源,需要自定义默认的ulimit。 检测加固:

代码语言:javascript复制
# 确保根据需要设置'--default-ulimit'参数
ps -ef| grep dockerd

# 在守护进程模式下运行docker,并根据相应的ulimits传递'--default-ulimit'作为参数。
dockerd --default-ulimit nproc=1024:2408 --default-ulimit nofile=100:200

# 在 daemon.json 中设置
"default-ulimits": {
    "nofile": {
        "Name": "nofile",
        "Hard": 1024000,
        "Soft": 1024000
    },
    "nproc": {
        "Name": "nproc",
        "Hard": 1024000,
        "Soft": 1024000
    },
    "core": {
        "Name": "core",
        "Hard": -1,
        "Soft": -1
  }
},

操作影响: 如果ulimits未正确设置,则可能无法实现所需的资源控制,甚至可能导致系统无法使用

默认值: 默认情况下,不设置ulimit

备注: 慎用

2.8 启用用户命名空间

描述: 在Docker守护进程中启用用户命名空间支持,可对用户进行重新映射。该建议对镜像中没有指定用户是有帮助的。如果在容器镜像中已经定义了非root运行,可跳过此建议,因为该功能比较新,可能会给带来不可预测的问题。 加固说明: Docker守护进程中对Linux内核用户命名空间支持为Docker主机系统提供了额外的安全性。它允许容器具有独特的用户和组ID,这些用户和组ID在主机系统所使用的传统用户和组范围之外。root用户希望有容器内的管理权限,可映射到主机系统上的非root的UID上。 检测加固:可参考Docke文档了解具体的配置方式。操作可能因平台而异在RedHat上,子UID和子GID映射创建不会自动工作。必须。确保存在/etc/subuid 、/etc/subgid 并使用–userns-remap标志启动docker守护进程

代码语言:javascript复制
# 执行命令将查找容器的PID,然后列出与容器进程关联的主机用户。如果容器进程以root身份运行,则不符合安全要求。
~$ ps -o pid,user,command `docker inspect -format='{{.State.Pid}}' $(docker ps -aq) | cut -d '=' -f 2`
    PID USER     COMMAND
  22429 root     /bin/bash /usr/local/bin/start.sh
  22619 10000    /home/chart/chartm
  22638 10000    nginx: master process nginx -g daemon off;


# 手动创建映射,手动设置docker启用标志启动
touch /etc/subuid /etc/subgid
dockerd --userns-remap=default

操作影响: 注意用户命名空间重新映射使得一些Docker功能不兼容,可查看Docker文档和参考链接以获取详细信息。

默认值: 默认情况下,用户命名空间不会重新映射。

2.9 使用默认cgroup

描述: 查看–cgroup-parent 选项允许设置用于所有容器的默认 cgroup parent。如果没有特定用例,则该设置应保留默认值。 加固说明: 系统管理员可定义容器应运行的cgroup。若系统管理员没有明确定义cgroup,容器也会在docker cgroup下运行。应该监测和确认使用情况。通过加到与默认不同的cgroup,导致不合理地共享资源,从而可能会主机资源耗尽。 检测加固:默认设置够用的话可保留。

代码语言:javascript复制
# 执行如下命令,确保'--cgroup-parent'参数未设置或设置为适当的非默认cgroup。
ps -ef | grep dockerd

# 如果要特别设置非默认cgroup,在启动时将-cgroup-parent参数传递给docker守护进程。
dockerd --cgroup-parent=/foobar

加固方法: 默认设置够用的话,可保留。如果要特别设置非默认cgroup,

操作影响: None

默认值: 如果未设置此选项,则默认为 /docker为了 fs cgroup 驱动程序和 system.slice用于 systemd cgroup 驱动程序。

如果 cgroup 有一个前导正斜杠 ( /),创建 cgroup 在根 cgroup 下,否则在 daemon 下创建 cgroup c组。假设守护进程在 cgroup 中运行 daemoncgroup, --cgroup-parent=/foobar在中创建一个 cgroup /sys/fs/cgroup/memory/foobar,而使用 –cgroup-parent=foobar 在中创建 cgroup /sys/fs/cgroup/memory/daemoncgroup/foobar

2.10 启用docker客户端命令的授权

描述: 使用本机Docker授权插件或第三方授权机制与Docker守护进程来管理对Docker客户端命令的访问。 加固说明: Docker默认是没有对客户端命令进行授权管理的功能。任何有权访问Docker守护进程的用户都可以运行任何Docker客户端命令。对于使用Docker远程API来调用守护进程的调用者也是如此。如果需要细粒度的访问控制,可以使用授权插件并将其添加到Docker守护进程配置中。使用授权插件,Docker管理员可以配置更细粒度访问策略来管理对Docker守护进程的访问。Docker的第三方集成可以实现他们自己的授权模型,以要求Docker的本地授权插件(即Kubernetes,CloudFoundry,Openshift)之外的Docker守护进程的授权。 检测加固:

代码语言:javascript复制
# 如果使用docker本地授权,可使用--authorization-plugin参数加载授权插件。
ps -ef | grep dockerd

# 加固流程
第1步:安装/创建授权插件。
第2步:根据需要配置授权策略。
第3步:重启docker守护进程

操作影响: 使用授权插件可能会导致性能下降。

默认值: 默认情况下,未设置授权插件。

2.11 配置集中和远程日志记录

描述: Docker现在支持各种日志驱动程序, 存储日志的最佳方式是支持集中式和远程日志记录。 加固说明: 集中和远程日志确保所有重要的日志记录都是安全的,以满足容灾的要求。Docker支持多种类型的日志驱动程序,可根据自身情况选取。 检测加固:

代码语言:javascript复制
# 运行docker info并确保日志记录驱动程序属性被设置为适当的。
docker info --format '{{.LoggingDriver}}'

# 可通过如下命令查看 --log-driver 的设置。
ps -ef | grep dockerd

# 加固流程
第1步:按照其文档设置所需的日志驱动程序。
第2步:使用该日志记录驱动程序启动docker守护进程。
dockerd --log-driver=syslog --log-optsyslog-address=tcp://10.10.107.233

# 或者
# 设置log-driver字段值为syslog并添到daemon.json 文件中
$ vim /etc/docker/daemon.json
"log-driver": "syslog",
  "log-level": "info",
  "log-opts": {
    "syslog-address": "tcp://10.10.107.233",
  },

操作影响: None

默认值: 默认情况下,容器日志为json文件格式

2.12 禁用docker resitry v1版本支持

描述: 最新的Docker registry版本是v2。v1版本存在很多安全问题,V1上的所有操作都应受到限制 加固说明: Docker镜像仓库v2在v1中引入了许多性能和安全性改进。它支持容器镜像来源验证和其他安全功能。因此对DockerV1仓库的操作应该受到限制。好在当前20.10.x已经不再针对仅支持旧版 v1 协议的注册表进行操作支持的,但是一些旧的版本是仍然支持的,此时可以采用如下方法禁用。 检测加固:

代码语言:javascript复制
# 下面的命令应该列出--disable-legacy-registry作为传递给docker守护进程的选项。
ps -ef | grep dockerd
grep "disable-legacy-registry" /etc/docker/daemon.json

# 在 daemon.json 文件中配置 disable-legacy-registry 字段
vim /etc/docker/daemon.json
"disable-legacy-registry": true,

操作影响: 旧版镜像仓库操作将受到限制

默认值: 默认情况下,允许旧版镜像仓库操作

备注

2.13 启用实时恢复

描述: 使用–live-restore参数可以支持无守护进程的容器运行。它确保Docker daemon在关闭或恢复时不会停止容器,并在重新启动后重新连接到容器。 加固说明: 可用性作为安全一个重要的属性, 在Docker守护进程中设置'--live-restore'标志可确保当docker守护进程不可用时容器执行不会中断,这也意味着当更新和修复docker守护进程而不会导致容器停止工作。 检测加固:

代码语言:javascript复制
# 确保LiveRestoreEnabled属性设置为true。
docker info --format '{{.LiveRestoreEnabled}}'

# 在 daemon.json 文件中配置live-restore字段
vim /etc/docker/daemon.json
"live-restore": true,

操作影响: None

默认值: 默认情况下–live-restore不启用

备注

2.14 禁用 userland 代理

描述: 当容器端口需要被映射时,docker守护进程都会启动用于端口转发的userland-proxy方式, 如果使用了DNAT方式则该功能可以禁用,是否使用用户态代理来实现容器间和出容器的回环通信。 加固说明: Docker引擎提供了两种机制将主机端口转发到容器,DNAT和userland-proxy。在大多数情况下,DNAT模式是首选,因为它提高了性能,并使用本地Linux iptables功能而需要附加组件, 如果DNAT可用,则应在启动时禁用userland-proxy以减少安全风险。 检测加固:

代码语言:javascript复制
# 确保--userland-proxy参数设置为false。
ps -ef | grep dockerd

# 运行Docker守护进程如下:
dockerd --userland-proxy=false

# 或者设置userland-proxy字段值为false并添到daemon.json 文件中
$ vim /etc/docker/daemon.json
 "userland-proxy": false,
 "userland-proxy-path": "/usr/libexec/docker-proxy",

操作影响: 某些旧版Linux内核的系统可能无法支持DNAT,因此需要userland-prox服务。此外,某些网络设置可能会因删除userland-prox而受到操作影响。

默认值: 默认情况下,userland-prox已启用。

备注:建议使用较新内核的Linux发行版

2.15 限制容器获取新的权限

描述: 默认情况下,限制容器通过suid或sgid位获取附加权限。 加固说明: 一个进程可以在内核中设置no_new_priv。它支持fork,clone和execve。no_new_priv确保进程或其子进程不会通过suid或sgid位获得任何其他特权。这样,很多危险的操作就降低安全风险。在守护程序级别进行设置可确保默认情况下,所有新容器不能获取新的权限。 检测加固:

代码语言:javascript复制
# 确保--no-new-privileges参数存在且未设置为false。
ps -ef | grep dockerd

# 运行Docker守护进程如下:
dockerd --no-new-privileges

# 设置no-new-privileges字段值为false并添到daemon.json 文件中
"no-new-privileges": true,

操作影响:no_new_priv会阻止像SELinux这样的LSM访问当前进程的进程标签。

默认值:默认情况下,容器不会获得新的权限。

备注


3.docker 守护进程文件配置

3.1 设置 docker.service 文件所属和权限

描述:验证’docker.service’文件所属和所属组是否正确设置为root。文件权限是否正确设置为’644’或更多限制。 加固说明:docker.service’文件包含可能会改变Docker守护进程行为的敏感参数。因此,它应该由root拥有和归属,以保持文件的完整性。 检测加固:

代码语言:javascript复制
# 查看docker.service文件属性,文件所属为root,权限为644
ls -l /lib/systemd/system/docker.service
stat -c %a-%U:%G /lib/systemd/system/docker.service
# 644-root:root

# 设置文件所属与权限所属
chown root:root /usr/lib/systemd/system/docker.service
chmod 644 /usr/lib/systemd/system/docker.service

操作影响:None.

默认值:该文件可能不存在于系统上。在这种情况下,此建议不适用。默认情况下,如果文件存在,则该文件的所属和所属组正确设置为root 权限为644。

备注

3.2 设置docker.socket文件所属和权限

描述:验证docker.socket文件所属和所属组是否正确设置为root,文件权限是否正确设置为’644’或更多限制。 加固说明:docker.socket文件包含可能会改变Docker远程API行为的敏感参数。因此,它应该拥有root权限,以保持文件的完整性。 检测加固:

代码语言:javascript复制
# 判断文件所属用户及用户组和权限
ls -al /usr/lib/systemd/system/docker.socket

# 所属和用户组应该为root,权限为644
chown root:root /usr/lib/systemd/system/docker.socket
chmod 644 /usr/lib/systemd/system/docker.socket

操作影响: None

默认值: 该文件可能不存在于系统上。在这种情况下,此建议不适用。默认情况下,如果文件存在,则该文件的所属和所属组正确设置为root, 文件权限正确设置为644。

备注

3.3 设置/etc/docker目录所有权为root:root

描述:验证/etc/docker目录所属和所属组是否正确设置为root, 权限是否正确设置为750或更多限制。 加固说明: 除了各种敏感文件之外/etc/docker目录还包含证书和密钥。 因此,它应该由root拥有和归组来维护目录的完整性。 检测加固:

代码语言:javascript复制
# 执行以下命令以验证该目录是由root拥有和归属的(所属和所属组设置为root,权限设置为750)
stat -c %U:%G /etc/docker | grep  root:root

# 将目录的所属和所属组设置为root,权限设置为750
chown root:root /etc/docker
chmod 750 /etc/docker

操作影响: None.

默认值: 默认情况下所属和所属组为 root,权限为755。

备注

3.4 设置仓库证书文件所有权为root:root

描述: 验证所有仓库证书文件/etc/docker/certs.d/<registry-name>所属和所属组是否为root,权限为600或更多限制的权限 加固说明: 在/etc/docker/certs.d/<registry-name>目录包含Docker镜像仓库证书,这些证书文件必须由root和其组拥有,以维护证书的完整性 检测加固:

代码语言:javascript复制
# 所属和所属组设置为root,权限设置为600
find /etc/docker/certs.d/ -type f -exec stat -c '%U:%G - %a - %n' {}   
root:root - 600 - /etc/docker/certs.d/harbor.weiyigeek.top/server.key
root:root - 600 - /etc/docker/certs.d/harbor.weiyigeek.top/harbor.weiyigeek.top.crt
root:root - 600 - /etc/docker/certs.d/hub.weiyigeek.top/hub.weiyigeek.top.crt

# 将镜像仓库证书文件的所属和所属组设置为root。
chown root:root /etc/docker/certs.d/<registry-name>/*
chmod -R 600  /etc/docker/certs.d/<registry-name>/

操作影响: None.

默认值: 默认情况下,镜像仓库证书文件的所属和所属组正确设置为root 权限为444.

备注

3.5 设置TLS CA证书文件所有权为root:root

描述: 验证TLSCA证书文件是由root拥有和分组拥有的,权限为444。 加固说明: TLS CA证书文件应受到保护,不受任何篡改。它用于指定的CA证书验证。因此,它必须由root拥有,权限为444,以维护CA证书的完整性。 检测加固:

代码语言:javascript复制
# 执行以下命令判断CA证书的所属和权限, 以验证TLS CA证书文件是否由root和其组拥有:
stat-c %U:%G <路径到TLS CA证书文件> | grep -v root:root

# 将TLS CA证书文件所属和所属组设置为root,权限为444。
chown root:root  -R <路径到TLS CA证书文件>
chmod 444 -R <路径到TLS CA证书文件>

操作影响: None.

默认值: 默认情况下,TLS CA证书文件的所有权和组属性正确设置为root。默认文件权限由系统或用户特定的umask值控制。

备注

3.6 设置docker服务器证书文件所有权为root:root

描述: 验证Docker服务器证书文件(与–tlscert’参数一起传递的文件)是否由root和其组拥有,权限为444。 加固说明: Docker服务器证书文件应受到保护,不受任何篡改。它用于验证Docker服务器。因此,它必须由root拥有以维护证书的完整性。 检测加固:

代码语言:javascript复制
# 上面的命令所属和所属组为root,权限为444
ls -al <Docker服务器证书文件的路径>

# 将docker服务器证书文件的所属和所属组设置为root。
chown root:root -R <路径到Docker服务器证书文件>
chmod 444 -R  <路径到Docker服务器证书文件>

操作影响:None.

默认值:默认情况下,Docker服务器证书文件的所属和所属组正确设置为root, 默认文件权限由系统或用户特定的umask值控制。

备注

3.7 设置docker服务器证书密钥文件所有权为root:root

描述: 验证Docker服务器证书密钥文件(与–tlskey’参数一起传递的文件)是由由root拥有,权限设置为400。

加固说明: Docker服务器证书密钥文件应受到保护,不受任何篡改或不必要的读取。它保存Docker服务器证书的私钥。因此它必须由root拥有,权限为400 以维护Docker服务器证书的完整性。 检测加固:

代码语言:javascript复制
# 所属和所属组为root,权限为400
ls -al <路径到Docker服务器证书密钥文件>

# 将Docker服务器证书密钥文件的所属和所属组设置为root,权限设置为400
chown root:root -R <路径到Docker服务器证书密钥文件>
chmod 400 -R <路径到docker服务器证书密钥文件>

操作影响 None. 默认值 默认情况下,Docker服务器证书密钥文件的所属和所属组正确设置为root。文件权限由系统或用户特定的umask值控制。 备注

3.8 设置/var/run/docker.sock文件所有权为root:docker

描述: 验证docker.sock文件由root拥有,而用户组为docker,权限为660。 加固说明:Docker守护进程以root用户身份运行。因此,默认的Unix套接字必须由root拥有。如果任何其他用户或进程拥有此套接字,那么该非特权用户或进程可能与Docker守护进程交互。另外,这样的非特权用户或进程可能与容器交互。这样非常不安全。另外,Docker安装程序会创建一个名为docker的用户组。可以将用户添加到该组,然后这些用户将能够读写默认的DockerUnix套接字。docker组成员由系统管理员严格控制。如果任何其他组拥有此套接字,那么该组的成员可能会与Docker守护进程交互。。 因此,默认的DockerUnix套接字文件必须由docker组拥有权限,以维护套接字文件的完整性。 检测加固:

代码语言:javascript复制
# 所属和所属组为root,权限为660
ls -al /var/run/docker.sock
~$ stat -c %a-%U:%G /var/run/docker.sock
660-root:docker

# 将所有权设置为root和组所有权到docker作为默认Docker套接字文件。
chown root:docker /var/run/docker.sock
chmod 660 /var/run/docker.sock

操作影响 None.

默认值 默认情况下,Docker套接字文件的所属和所属组正确设置为root:docker,权限正确设置为’660’

3.9 设置daemon.json文件所有权为root:root

描述: 验证daemon.json文件所属和所属组是否正确设置为root,文件权限是否正确设置为644或更多限制。 加固说明: daemon.json文件包含可能会改变docker守护进程行为的敏感参数。因此,它应该由root拥有,以维护文件的完整性。 检测加固:

代码语言:javascript复制
# 文件所属和所属组为root,权限为644
ls -l /etc/docker/daemon.json
stat -c %a-%U:%G /etc/docker/daemon.json
644-root:root

# 将文件的所属和所属组设置为root,权限为644。
chown root:root /etc/docker/daemon.json
chmod 644 /etc/docker/daemon.json

操作影响 None.

默认值: 该文件可能不存在于系统上。在这种情况下,此建议不适用.文件权限由系统或用户特定的umask值控制。

备注

3.10 设置 /etc/default/docker 文件所有权为 root:root

描述: 验证/etc/default/docker文件所属和所属组是否正确设置为root,文件权限是否正确设置为644或更多限制。 加固说明: /etc/default/docker文件包含可能会改变docker守护进程行为的敏感参数。因此,它应该由root拥有,以维护文件的完整性。 检测加固:

代码语言:javascript复制
# 文件所属和所属组为root,权限为644
ls -l /etc/default/docker
stat -c %a-%U:%G /etc/default/docker

# 这将文件的所属和所属组设置为root。
chown root:root/etc/default/docker

操作影响: None.

默认值: 该文件可能不存在于系统上。在这种情况下,此建议不适用。

备注

4.容器镜像和构建文件

4.1 创建容器的用户

描述: 为容器镜像的Dockerfile中的容器创建非root用户。 加固说明: 如果可能,最好指定非root用户身份运行容器。虽然用户命名空间映射可用,但是如果用户在容器镜像中指定了用户,则默认情况下容器将作为该用户运行,并且不需要特定的用户命名空间重新映射。 检测加固

代码语言:javascript复制
# 如果为空则表示容器以root身份运行。
docker ps -q |xargs docker inspect --format '{{.Id}}:User={{.Config.User}}'
# 确保容器镜像的Dockerfile包含以下指令:USER<用户名或ID>其中用户名或ID是指可以在容器基础镜像中找到的用户。如果在容器基础镜像中没有创建特定用户,则在USER指令之前添加user add命令以添加特定用户。
RUN user add -d /home/app -m -s/bin/bash app 
USER app
``` 
注意:如果镜像中有容器不需要的用户,应删除它们。删除这些用户后,提交镜像,然后生成新的容器实例以供使用。
操作影响: None.
默认值: 默认情况下,容器以root权限运行,并以容器中的用户root身份运行。

<br>

#### 4.2 容器使用可信的基础镜像
描述: 确保容器镜像是从头开始编写的,或者是基于通过安全仓库下载的另一个已建立且可信的基本镜像。
加固说明: 官方存储库是由Docker社区或供应商优化的Docker镜像。可能还存在其他不安全的公共存储库。在从Docker和第三方获取容器镜像时,需谨慎使用。
检测方法: 检查Docker主机以查看执行以下命令使用的Docker镜像:docker images这将列出当前可用于Docker主机的所有容器镜像。再对在Docker主机上找到的每个Docker镜像,检查镜像的构建方式,以验证是否来自可信来源
判断方法: 判断镜像来源的合法性
加固方法: 配置和使用Docker内容信任。检查Docker镜像历史记录以评估其在网络上运行的风险。使用镜像扫描工具扫描Docker镜像以查找其依赖关系中的漏洞。
操作影响: None.
默认值: 无


<br>

#### 4.3 容器中不安装没有必要的软件包
描述: 选用精简的镜像作为基础镜像,不安装不必要的软件
加固说明: 安装不必要的软件可能会增加容器的攻击风险。因此,除了容器的真正需要的软件之外,不要安装其他多余的软件。
检测方法: 进入容器中执行命令检查安装的软件包
判断方法: 查看软件包列表并确保它是合法的。
加固方法: 建议选用alpine镜像或官方Linux发行版精简的镜像
操作影响: None.
默认值: 不适用。

<br>

#### 4.4 扫描镜像漏洞并且构建包含安全补丁的镜像
描述: 应该经常扫描镜像以查找漏洞。重建镜像安装最新的补丁。
加固说明: 安全补丁可以解决软件的安全问题。可以使用镜像漏洞扫描工具来查找镜像中的任何类型的漏洞,然后检查可用的补丁以减轻这些漏洞。修补程序将系统更新到最新的代码库。
检测方法: 运行镜像漏洞扫描工具扫描镜像
判断方法: 检查扫描结果中存在的安全隐患
加固方法: 
* 第1步:取出所有基本镜像(即给定一组Dockerfiles,提取在FROM指令中声明的所有镜像,并重新提取它们以检查更新
* 第2步:重建每个镜像:docker build --no-cache
* 第3步:使用更新的镜像重新启动所有容器,还可以在Dockerfile中使用ONBUILD指令来触发经常用作基本镜像的特定更新指令。
操作影响: None.
默认值: 默认情况下,容器和镜像不会自动更新。
备注: 如果镜像漏洞扫描工具可以执行二进制级别分析,而不仅仅是版本字符串匹配,则会更好。


<br>

#### 4.5 启用docker内容信任
描述: 默认情况下禁用内容信任,为了安全起见,可以启用。
加固说明: 内容信任为向远程Docker镜像仓库发送和接收的数据提供了使用数字签名的能力。这些签名允许客户端验证特定镜像标签的完整性和发布者。这确保了容器镜像的来源的合法性。
检测加固: 
```bash
# DOCKER_CONTENT_TRUST 环境变量执行应该返回1
echo $DOCKER_CONTENT_TRUST

# 要在bash shell中启用内容信任或者,在配置文件中设置此环境变量
export DOCKER_CONTENT_TRUST=1

操作影响: 在设置了DOCKER_CONTENT_TRUST的环境中,需要在处理镜像时遵循信任过程-构建,创建,拉取,推送和运行。可以使用–disable-content-trust标志按照需要在标记镜像上运行单独的操作,一般用于测试目的,生成环境中应尽不要使用。

默认值: 默认情况下,内容信任被禁用。

4.6 将HEALTHCHECK说明添加到容器镜像

描述: 在Docker容器镜像中添加HEALTHCHECK指令以对正在运行的容器执行运行状况检查。 加固说明: 安全性最重要的一个特性就是可用性。将HEALTHCHECK指令添加到容器镜像可确保docker引擎定期检查运行的容器实例是否符合该指令,以确保实例仍在运行。根据报告的健康状况,docker引擎可以退出非工作容器并实例化新容器。 检测加固:

代码语言:javascript复制
# 运行以下命令,并确保docker镜像对HEALTHCHECK指令设置, 应当返回设置值
docker inspect --format='{{.Config.Healthcheck}}' <镜像ID>
~$ docker inspect --format='{{.RepoTags}} - {{.Config.Healthcheck}}' $(docker images -q)
[weiyigeek/go-webserver:v2.0] - {[CMD-SHELL wget --spider http://localhost:8080 || exit 1] 5s 3s 0s 5}
[weiyigeek/go-webserver:v1.0 hub.weiyigeek.top/go-webserver:v1.0] - {[CMD-SHELL curl -fs http://localhost:8080 || exit 1] 5s 3s 0s 5}
[weiyigeek/go-webserver:v1.0 hub.weiyigeek.top/go-webserver:v1.0] - {[CMD-SHELL curl -fs http://localhost:8080 || exit 1] 5s 3s 0s 5}
[ubuntu-htop:v0.1] - <nil>

# 按照Dockerfile文档,并使用HEALTHCHECK指令重建容器镜像。
HEALTHCHECK --interval=5s --timeout=3s --retries=5 
  CMD curl -fs http://localhost:8080/ || exit 1

操作影响: None.

默认值: 默认情况下,HEALTHCHECK未设置。

4.7 不在dockerfile中单独使用更新命令

描述: 不要单独使用apt-get upgradeyum makecache等更新指令也不要在Dockerfile中使用更新指令。 加固说明: 在Dockerfile添加更新指令将缓存更新的层。稍后使用相同的指令构建任何镜像时,将使用先前缓存的更新图层。这可能会拒绝任何新版本进入到以后的版本。 检测加固:

代码语言:javascript复制
# 查看Dockerfile,请确认没有上述更新指示。
docker images 
docker history <Image_ID> | grep "upgrade"

# 在安装软件包时,请使用最新的固定版本软件包。或可以在docker构建过程中使用--no-cache标志,以避免使用缓存的层。

操作影响: None.

默认值: 默认情况下,docker对更新无限制。

4.8 镜像中删除setuid和setgid权限

描述: 删除镜像中的setuid和setgid权限防止容器中的提权攻击。 加固说明: setuid和setgid可用于提升权限。虽然这些权限有时必须,应考虑为镜像中不需要的软件包删除这些权限。 检测加固:

代码语言:javascript复制
# 在镜像上运行以下命令以列出具有setuid和setgid权限的可执行文件, 仔细检查列表确保它是合法的
docker run <Image_ID> find / -perm  6000 -typef -exec ls -ld {}; 2>/dev/null

# 只在需要可执行的文件上允许setuid和setgid权限。可在构建时通过在Dockerfile中添加以下命令来删除这些权限,最好添加在Dockerfile的末尾:
RUN find /- perm  6000 -typef -exec chmod a -s {};||true
```	
操作影响: 以上命令会导致依赖setuid或setgid权限(包括合法权限)的可执行文件无法执行,需要小心处理。
默认值: None

<br>

#### 4.9 在dockerfile中使用copy而不是add
描述: 在Dockerfile中使用COPY指令而不是ADD指令。
加固说明: COPY指令只是将文件从本地主机复制到容器文件系统。ADD指令可能会从远程URL下载文件并执行诸如解包等操作。因此,ADD指令增加了从URL添加恶意文件的风险。
检测方法: 通过`docker history <Image_ID>`或`Dockerfile`
查找构建镜像过程中是否使用了ADD指令
判断方法: 不允许存在ADD指令
加固方法: 在Dockerfiles中使用COPY指令。
操作影响: 可能需ADD指令提供的功能,从远程URL获取文件。
默认值: none

<br>

#### 4.10 涉密信息不存储在dockerfile
描述: 不要在Dockerfiles中存储任何涉密信息。
加固说明: 通过使用docker history历史命令,可以查看各种工具和实用程序。镜像发布者提供Dockerfiles来构建镜像。所以,Dockerfiles中的涉密信息可能会被暴露并被恶意利用。
检测方法: `docker history <Image_ID>` 或查看Dockerfile查找是否有涉密信息
判断方法: 不应该有涉密的信息,如用户账号,私钥证书等。
加固方法: 不要在Dockerfiles中存储任何类型的涉密信息。
操作影响: 若必须使用,需要制定相应的措施
默认值: 默认情况下,在Dockerfiles中存储配置密码没有限制。

<br>

#### 4.11 仅安装已经验证的软件包
描述: 在将软件包安装到镜像中之前,验证软件包可靠性。
加固说明: 验证软件包的可靠性对于构建安全的容器镜像至关重要。不合法的软件包可能具有恶意或者存在一些可能被利用的已知漏洞。
检测方法: `docker history <Image_ID>`或查看Dockerfile查看软件包的合法性
加固方法: 使用GPG密钥下载和验证所选择的软件包。
操作影响: None
默认值: 不适用



<br>

#### 4.12 容器内部项目指定运行用户
_Q:在说此项前我们先来了解容器内部应用程序运行用户设置建议采用gosu还是sudo?_
答:我们通过实战来确定到底使用哪一个命令较好;
```bash
# gosu
docker run --rm gosu/alpine gosu root ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 ps aux  # 通过gosu启动的是符合我们要求的(PID为1)容器内的唯一进程

# sudo
docker run --rm ubuntu:trusty sudo ps aux
#容器内出现了两个进程,sudo命令会创建第一个进程,然后该进程再创建了ps进程,而且ps进程的PID并不等于1,这是达不到我们要求的,此时在宿主机向该容器发送信号量收到信号量的是sudo进程;
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.0  46012  1772 ?        Rs   12:05   0:00 sudo ps aux
root          6  0.0  0.0  15568  1140 ?        R    12:05   0:00 ps aux

小结:

  • gosu 启动命令时只有一个进程,所以docker容器启动时使用gosu,那么该进程可以做到PID等于1;
  • sudo 启动命令时先创建sudo进程,然后该进程作为父进程去创建子进程,1号PID被sudo进程占据;

正题回归

描述:为了安全容器中不要使用root账号(即最小权限),此时就需要一个能够提升自定账号权限的命令 gosu 便应运而生它与 sudo 类似但它比sudo更安全;

方便学习先来看看一个小例子(在镜像中创建非root账号):

首先我们以redis官方镜像的Dockerfile为例,来看看如何创建账号

代码语言:javascript复制
#1.先添加我们的用户和组,以确保他们的id被一致地分配,不管添加了什么依赖项
RUN groupadd -r redis && useradd -r -g redis redis

#2.可见redis官方镜像使用groupadd和useradd创建了名为redis的组合账号,接下来就是用redis账号来启动服务了,理论上应该是以下套路;
* 用USER redis将账号切换到redis;
* 在docker-entrypoint.sh执行的时候已经是redis身份了,如果遇到权限问题,例如一些文件只有root账号有读、写、执行权限,用sudo xxx命令来执行即可;

但事实并非如此!

在Dockerfile脚本中未发现USER redis命令,意味着执行docker-entrypoint.sh文件的身份是root;

其次在docker-entrypoint.sh中没有发现su - redis命令,也没有sudo命令

这是怎么回事呢?难道容器内的redis服务是用root账号启动的?

代码语言:javascript复制
#动手实践
docker run --name myredis -idt redis
docker exec -it myredis /bin/bash
apt-get update && apt-get install procps #更新软件源以及PS命令安装
root@122c2df16bbb:/data# ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
redis         1      0  0 09:22 ?        00:00:01 redis-server *:6379
root        287      0  0 09:36 ?        00:00:00 /bin/bash
root        293    287  0 09:39 ?        00:00:00 ps -ef

上面的结果展示了两个关键信息: 第一,redis服务是redis账号启动的并非root; 第二,redis服务的PID等于(重要),宿主机执行docker stop命令时,该进程可以收到SIGTERM信号量,于是redis应用可以做一些退出前的准备工作,例如保存变量、退出循环等,也就是优雅停机(Gracefully Stopping);

现在我们已经证实了redis服务并非root账号启动,而且该服务进程在容器内还是一号进程,但是我们在Dockerfile和docker-entrypoint.sh脚本中都没有发现切换到redis账号的命令,也没有sudo和su,这是怎么回事呢?

答案:在于redis的docker-entrypoint.sh文件之中

代码语言:javascript复制
#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
  set -- redis-server "$@"
fi

# allow the container to be started with `--user` | 首次运行进入
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
  # 当前文件夹下不属于redis用户的文件,全部授权为redis用户
  find . ! -user redis -exec chown redis '{}'  
  exec gosu redis "$0" "$@"
fi

exec "$@"

注意上图中的代码分析一下:

1.假设启动容器的命令是docker run --name myredis -idt redis redis-server /usr/local/etc/redis/redis.conf

2.容器启动后会执行docker-entrypoint.sh脚本此时的账号是root;

3.当前账号是root因此进入后会执行第二个if条件中命令,其中位置参数含税表示如下;

  • $0表示当前脚本的名称即docker-entrypoint.sh;
  • $@表示外部传入的所有参数,即redis-server /usr/local/etc/redis/redis.conf;

表示以redis账号的身份执行以下命令

代码语言:javascript复制
# gosu redis "$0" "@"
docker-entrypoint.sh redis-server /usr/local/etc/redis/redis.conf

gosu redis "$0" "@"前面加上个exec,表示以gosu redis “$0” “@”这个命令启动的进程替换正在执行的 docker-entrypoint.sh 进程 保证了对应的进程ID为1

gosu redis "$0" "@"导致docker-entrypoint.sh再执行一次,但是当前的账号已经不是root了,所以会执行兜底逻辑 exec “$@”;

此时的$@是redis-server /usr/local/etc/redis/redis.conf,因此redis服务会启动并且启动服务的用户为redis;

最后我们在 Redis 的Dockerfile 中可以看见安装gosu的一些身影

代码语言:javascript复制
# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.14
RUN set -ex; 
	
	fetchDeps=" 
		ca-certificates 
		dirmngr 
		gnupg 
		wget 
	"; 
	apt-get update; 
	apt-get install -y --no-install-recommends $fetchDeps; 
	rm -rf /var/lib/apt/lists/*; 
	
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; 
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; 
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; 
	export GNUPGHOME="$(mktemp -d)"; 
	gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; 
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; 
	gpgconf --kill all; 
	rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; 
	chmod  x /usr/local/bin/gosu; 
	gosu nobody true; 
	
	apt-get purge -y --auto-remove $fetchDeps

5.容器运行时保护

5.1 设置SElinux安全选项

描述: SELinux是一个有效且易于使用的Linux访问控制机制。 加固说明: SELinux提供强制访问控制(MAC)系统,大大增强了默认的自由访问控制(DAC)模型。因此,可以通过在Linux主机上启用SELinux(如果适用)来增加额外的安全防护。 检测方法:

代码语言:javascript复制
# 检查方式
$ ~$ docker ps -q -a | xargs docker inspect --format 'Name = {{.Name}}, SecurityOpt = {{json .HostConfig.SecurityOpt}}'
Name = /nginx, SecurityOpt = null
Name = /harbor-jobservice, SecurityOpt = null

判断方法: 上述命令应返回当前为容器配置的所有安全选项。

加固方法: 如果SELinux适用于你的Linux操作系统,请使用它,可能需要遵循以下步骤:

1.设置SELinux状态。

2.设置SELinux策略。

3.为Docker容器创建或导入SELinux策略模板。

4.启用SELinux的守护进程模式下启动Docker。docker daemon –selinux-enabled

5.使用安全选项启动Docker容器。docker run --interactive—tty --security-optlabel=level TopSecretcentos /bin/bash

代码语言:javascript复制
# 1.设置selinux-enabled字段值为true并添到daemon.json 文件中
"selinux-enabled": true,

操作影响: selinux配置文件中定义的一组操作限制。如果配置错误,容器可能无法完成工作。 默认值: 默认情况下,在容器上不应用SELinux安全选项。

5.2 linux内核特性在容器内受限

描述: 默认情况下,Docker使用一组受限制的Linux内核特性启动容器, 这意味着可以将任何进程授予所需的功能,而不是root访问, 使用Linux内核特性,这些进程不必以root权限运行。(为容器运行中的任何进程授予最小所需功能) 加固说明: Docker支持添加和删除功能,允许使用非默认配置文件。这可能会使Docker通过移除功能更加安全,或者通过增加功能来减少安全性。因此,建议除去容器进程明确要求的所有功能。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a  | xargs docker inspect --format '{{.Id}}:CapAdd={{.HostConfig.CapAdd}}CapDrop={{.HostConfig.CapDrop}}'
72b163c7e64e66fcecd09336bde28ee393cf07ca80c091c920eeb50c2f596bf8:CapAdd=<no value>CapDrop=<no value>
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:CapAdd=[CHOWN SETGID SETUID NET_BIND_SERVICE]CapDrop=[ALL]

~$ docker ps -q -a | xargs docker inspect --format 'Name = {{.Name}}, CapAdd={{json .HostConfig.CapAdd}}, CapDrop={{json .HostConfig.CapDrop}}'
Name = /nginx, CapAdd=["CHOWN","SETGID","SETUID","NET_BIND_SERVICE"], CapDrop=["ALL"]
Name = /harbor-jobservice, CapAdd=["CHOWN","SETGID","SETUID"], CapDrop=["ALL"]

判断方法: 验证添加和删除的Linux内核功能是否符合每个容器实例的容器进程所需的功能。

加固方法:

代码语言:javascript复制
# 1.分别使用--cap-add和--cap-drop参数来增加所需的功能或者移除所需功能。
$ docker run --cap-add={Capability1,Capability2} --cap-drop=all  # 白名单机制
$ docker run -it --name captest --cap-add={chown,net_bind_service,net_admin} --cap-drop=all ubuntu:latest /bin/bash

# 2.在容器内部查询支持那些capability
$ capsh --print
Current: = cap_chown,cap_net_bind_service,cap_net_admin eip
Bounding set =cap_chown,cap_net_bind_service,cap_net_admin

$ docker ps -q -a | xargs docker inspect --format '{{.Name}}, CapAdd={{json .HostConfig.CapAdd}}, CapDrop={{json .HostConfig.CapDrop}}'
captest, CapAdd=["chown","net_bind_service","net_admin"], CapDrop=["all"]

操作影响: 基于添加或删除的Linux内核功能,容器中功能会受到限制。

默认值: 默认情况下以下功能可用于容器,参考containerd项目地址 (https://github.com/containerd/containerd/blob/main/oci/spec.go)

代码语言:javascript复制
func defaultUnixCaps() []string {
  return []string{
    "CAP_CHOWN",
    "CAP_DAC_OVERRIDE",
    "CAP_FSETID",
    "CAP_FOWNER",
    "CAP_MKNOD",
    "CAP_NET_RAW",
    "CAP_SETGID",
    "CAP_SETUID",
    "CAP_SETFCAP",
    "CAP_SETPCAP",
    "CAP_NET_BIND_SERVICE",
    "CAP_SYS_CHROOT",
    "CAP_KILL",
    "CAP_AUDIT_WRITE",
  }
}
5.3 不使用特权容器

描述: 使用–privileged标志将所有Linux内核功能提供给容器,从而覆盖-cap-add和-cap-drop标志。若无必须请不要使用它。 加固说明: –privileged标志给容器提供所有功能,并且还提升了cgroup控制器执行的所有限制, 若无必须不要使用它。 检测加固:

代码语言:javascript复制
# 命令应为每个容器实例返回Privileged=
docker ps -q -a | xargs docker inspect --format '{{.Id}}:Privileged={{.HostConfig.Privileged}}'
72b163c7e64e66fcecd09336bde28ee393cf07ca80c091c920eeb50c2f596bf8:Privileged=false
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Privileged=false

# 不要运行带有--privileged标志的容器。
~$ docker ps -q -a | xargs docker inspect --format '{{.Name}}:Privileged={{.HostConfig.Privileged}}'
/captest:Privileged=false
/cap_test:Privileged=false
/nginx:Privileged=false
/harbor-jobservice:Privileged=false

操作影响: 除默认值之外的Linux内核功能将无法在容器内使用。

默认值: False.

5.4 敏感的主机系统目录不要挂载在容器上

描述: 不应允许将敏感的主机系统目录(如/boot /dev /etc /lib /proc /sys /usr)作为容器卷进行挂载,特别是在读写模式下。 加固说明: 如果敏感目录以读写方式挂载,则可以对这些敏感目录中的文件进行更改。这些更改可能会降低安全性,且直接操作影响Docker宿主机。 检测方法: 命令将返回当前映射目录的列表,以及是否以每个容器实例的读写模式进行挂载。

代码语言:javascript复制
# 查看创建运行的容器挂载的主机系统目录
~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:Volumes={{.Mounts}}'
72b163c7e64e66fcecd09336bde28ee393cf07ca80c091c920eeb50c2f596bf8:Volumes=[map[Destination:/var/run/docker.sock Mode: Propagation:rprivate RW:true Source:/var/run/docker.sock Type:bind]]
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Volumes=[map[Destination:/etc/cert Mode:z Propagation:rprivate RW:true Source:/data/harbor/secret/cert Type:bind] map[Destination:/etc/nginx Mode:z Propagation:rprivate RW:true Source:/opt/harbor/common/config/nginx Type:bind] map[Destination:/harbor_cust_cert Mode: Propagation:rprivate RW:true Source:/opt/harbor/common/config/shared/trust-certificates Type:bind] map[Destination:/run Driver:local Mode: Name:2cbf86a1597dc82401969b05222e10b266d28c74382e97e1d402160c527a7633 Propagation: RW:true Source:/app/docker/volumes/2cbf86a1597dc82401969b05222e10b266d28c74382e97e1d402160c527a7633/_data Type:volume] map[Destination:/var/cache/nginx Driver:local Mode: Name:0955b631512898189af6c65b47098f6a791733f8ad560830726105608ba675ff Propagation: RW:true Source:/app/docker/volumes/0955b631512898189af6c65b47098f6a791733f8ad560830726105608ba675ff/_data Type:volume] map[Destination:/var/log/nginx Driver:local Mode: Name:afab7f34d4fa4dbcfae33ceadc0bc8b17fbfb79abdeabecda6cbe30cd861bef6 Propagation: RW:true Source:/app/docker/volumes/afab7f34d4fa4dbcfae33ceadc0bc8b17fbfb79abdeabecda6cbe30cd861bef6/_data Type:volume]]

# 排查那些容器挂载了主机敏感目录
~$ docker ps -q -a | xargs docker inspect --format '{{.Name}}: Volumes={{json .Mounts}}' | grep -E '"Source":"(/boot|/dev|/etc|/lib|/proc|/sys|/usr)'

加固方法: 不要将主机敏感目录挂载在容器上,尤其是在读写模式下。

操作影响: None.

默认值: Docker默认为读写卷,也可以以只读方式挂载一个目录。默认情况下,不会在容器上挂载敏感的主机目录。

5.5 SSH 不在容器中运行

描述: SSH服务不应该在容器内运行。 加固说明: 在容器内运行SSH可以增加安全管理的复杂性难以管理SSH服务器的访问策略和安全合规性难以管理各种容器的密钥和密码难以管理SSH服务器的安全升级可以在不使用SSH的情况下对容器进行shell访问,避免不必要地增加安全管理的复杂性。 检测方法: 对于每个容器实例,执行以下命令 docker exec $INSTANCE_ID ps -el 判断方法: 确保容器内没有SSH服务 加固方法: 从容器中卸载SSH服务 操作影响: None. 默认值: 默认情况下,SSH服务不在容器内运行

5.6 特权端口禁止映射到容器内

描述: 低于1024的TCP/IP端口号被认为是特权端口, 由于各种安全原因,普通用户和进程不允许使用它们。 加固说明: 默认情况下,如果用户没有明确声明容器端口进行主机端口映射,Docker会自动地将容器端口映射到主机上的49153-65535中。但是,如果用户明确声明它,Docker可以将容器端口映射到主机上的特权端口。这是因为容器使用不限制特权端口映射的NET_BIND_SERVICELinux内核功能来执行。特权端口接收和发送各种敏感和特权的数据。允许docker使用它们可能会带来严重的操作影响。

检测方法:

代码语言:javascript复制
# 通过执行以下命令列出容器的所有运行实例及其端口映射
$ docker ps -q | xargs docker inspect --format '{{.Id}}:Ports={{.NetworkSettings.Ports}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Ports=map[4443/tcp:[map[HostIp:0.0.0.0 HostPort:4443]] 8080/tcp:[map[HostIp:0.0.0.0 HostPort:80]] 8443/tcp:[map[HostIp:0.0.0.0 HostPort:443]]]
b769645200f1eb51bb04ad65b9d9fd122b017ebb7ec707437beb9883ce117285:Ports=map[10514/tcp:[map[HostIp:127.0.0.1 HostPort:1514]]]

判断方法: 查看列表,并确保容器端口未映射到低于1024的主机端口号。

加固方法: 启动容器时,不要将容器端口映射到特权主机端口。另外,确保没有容器在Docker文件中特权端口映射声明。

操作影响: None.

默认值: 默认情况下,允许将容器端口映射到主机上的特权端口。

备注: 有些端口是必须使用的HTTP和HTTPS必须绑定80/tcp和443/tcp。

5.7 只映射必要的端口

描述: 容器镜像的Dockerfile定义了在容器实例上默认要打开的端口。端口列表可能与在容器内运行的应用程序相关。 加固说明: 一个容器可以运行在Docker文件中为其镜像定义的端口,也可以任意传递运行时参数以打开一个端口列表。此外,Docker文件可能会进行各种更改,暴露的端口列表可能与在容器内运行的应用程序不相关。推荐做法是不要打开不需要的端口。 检测方法: 通过执行以下命令列出容器的所有运行实例及其端口映射:

代码语言:javascript复制
docker ps -q | xargs docker inspect --format '{{.Id}}:Ports={{.NetworkSettings.Ports}}'

判断方法: 查看列表,并确保映射的端口是容器真正需要的端口。

加固方法: 修复容器镜像的Dockerfile,以便仅通过容器化应用程序公开所需的端口。也可以通过在启动容器时不使用-P(UPPERCASE)或–publish -a 标志来完全忽略Dockerfile中定义的端口列表。使用-p明确定义特定容器实例所需的端口。

操作影响: None.

默认值: 默认情况下,当使用’-P’或’–publish -a ‘标志运行容器时,打开在EXPOSE指令下的Dockerfile中列出的所有端口。

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

描述: 当设置为–net=host时,容器上的网络模式将容器放置在单独的网络堆栈中。这个选择告诉Docker不使用Docker内部网络,那就意味着容器在可以完全访问主机的网络接口。 加固说明: 这有一定的安全风险,允许容器进程像任何其他root进程一样打开低端端口。还允许容器访问Docker主机上的D-bus等网络服务。因此容器进程可以潜在地执行恶意的事,关闭Docker主机。若无需要不要使用host模式。 检测方法:

代码语言:javascript复制
# 当命令执行返回 NetworkMode=host时则意味着在启动容器时传递了--net=host选项。
~$ docker ps -q -all | xargs docker inspect --format '{{.Id}}:NetworkMode={{.HostConfig.NetworkMode}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:NetworkMode=harbor_harbor

加固方法: 启动容器时不要通过’–net=host’选项。

操作影响: None.

默认值: 默认情况下,容器连接到Docker网桥。

5.9 确保容器的内存使用合理

描述: 默认情况下,Docker主机上的所有容器均等共享资源。通过使用Docker主机的资源管理功能,内存限制,可以控制容器可能消耗的内存量。 加固说明: 默认情况下,容器可以使用主机上的所有内存。可以使用内存限制机制来防止由于一个容器消耗了所有主机资源而导致拒绝服务,以致同一主机上的其他容器无法执行预期功能。 检测方法:

代码语言:javascript复制
:~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:Memory={{.HostConfig.Memory}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Memory=0

判断方法: 如果上述命令返回0,则表示内存无限制。如果上述命令返回非零值,则表示已有内存限制策略。

加固方法: 建议使用–memory参数运行容器。

操作影响: 如果有设置适当的限制,容器可能将无法使用。

默认值: 默认情况下没有内存限制。

5.10 正确设置容器上的CPU优先级

描述: 默认情况下,Docker主机上的所有容器均可共享资源。通过使用 Docker主机的资源管理功能(如CPU共享),可以控制容器可能占用的主机CPU资源。 加固说明: 默认情况下,CPU时间在容器间平均分配。如果需要,为了控制容器实例之间的CPU时间,可以使用CPU共享功能。CPU共享允许将一个容器优先于另一个容器,并禁止较低优先级的容器更频繁占用CPU资源。可确保高优先级的容器更好地运行。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a|xargs docker inspect --format '{{.Id}}:CpuShares= {{.HostConfig.CpuShares}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:CpuShares= 0

判断方法: 如果上述命令返回0或1024,则表示CPU无限制。如果上述命令返回非1024值以外的非零值,则表示CPU已经限制。

加固方法: 使用–cpu-shares参数启动容器。

操作影响: 如果没有设置适当的CPU共享,容器进程可能会不能执行。

默认值: 默认情况下没有执行CPU份额限制。

5.11 确保进入容器的流量绑定到特定的网卡

描述: 默认情况下,Docker容器可以连接到外部,但外部无法连接到容器。每个传出连接都源自主机自己的IP地址。所以只允许通过主机上的特定外部接口访问容器服务。 加固说明: 如果主机上有多个网卡,则容器可以接受任何网络接口上公开端口的连接。这可能不安全。很多时候,特定的端口暴露在外部,并且在这些端口上运行诸如入侵检测,入侵防护,防火墙,负载均衡等服务以筛选传入的公共流量。因此,只允许来自特定外部接口的传入连接。 检测方法: 通过执行以下命令列出容器的所有运行实例及其端口映射

代码语言:javascript复制
~$ docker ps -q | xargs docker inspect --format '{{.Id}}:Ports={{.NetworkSettings.Ports.HostIp}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Ports=map[4443/tcp:[map[HostIp:0.0.0.0 HostPort:4443]] 8080/tcp:[map[HostIp:0.0.0.0 HostPort:80]] 8443/tcp:[map[HostIp:0.0.0.0 HostPort:443]]]

判断方法: 查看列表并确保公开的容器端口与特定接口绑定,而不是通配符IP地址-0.0.0.0。

加固方法: 将容器端口绑定到所需主机端口上的特定网卡。

默认值: 默认情况下,Docker将容器端口公开在0.0.0.0,可接受主机上任何可能的传入网络端口。

操作影响: None.

5.12 容器重启策略on-failure设置为5

描述: 在docker run命令中使用 –restart标志,可以指定重启策略,以便在退出时确定是否重启容器。基于安全考虑,应该设置重启尝试次数限制为5次。 加固说明: 如果无限期地尝试启动容器,可能会导致主机上的拒绝服务。这可能是一种简单的方法来执行分布式拒绝服务攻击,特别是在同一主机上有多个容器时。此外,忽略容器的退出状态并始终尝试重新启动容器导致未调查容器终止的根本原因。如果一个容器被终止,应该做的是去调查它重启的原因,而不是试图无限期地重启它。因此,建议使用故障重启策略并将其限制为最多5次重启尝试。 检测方法:

代码语言:javascript复制
# 在 Docker run 上使用 --restart 标志,您可以指定容器在退出时应该或不应该如何重新启动的重新启动策略。
docker run --restart=on-failure:5  # 此处设置重试数为5次。

# 查看当前容器异常导致的重启次数
~$ docker ps -q  -a | xargs docker inspect -f "{{ .RestartCount }}".

# 查看重启策略及其重试次数
~$ docker ps -q  -a | xargs docker inspect --format '{{.Id}}:RestartPolicyName={{.HostConfig.RestartPolicy.Name}}, MaximumRetryCount={{.HostConfig.RestartPolicy.MaximumRetryCount}}'  # 
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:RestartPolicyName=always,MaximumRetryCount=0
b28b6bd4264d9aad4eff7214df6d368c44b5c252a6d61bb7fd85ebc75ffdc957:RestartPolicyName=always,MaximumRetryCount=0

判断方法: 如果上述命令返回RestartPolicyName=always,那么系统没有按需要进行配置。如果上述命令返回RestartPolicyName=no或仅RestartPolicyName=,则重新启动策略未被使用,容器不会重新启动。如果上述命令返RestartPolicyName=onfailure,则通过查看MaximumRetryCount验证重新启动尝试的次数是否设置为5或更少。

加固方法: 在docker run 或 docker-compos e中设定容器重启次数

操作影响: 容器只会尝试重新启动5次。

默认值: 默认情况下,容器未配置重新启动策略。

5.13 确保主机的进程命名空间不共享

描述: 进程PID命名空间隔离进程,不同PID命名空间中的进程可以具有相同的PID。这就是容器和主机之间的进程级隔离。 加固说明: PID名称空间提供了进程的隔离。PID命名空间删除了系统进程的视图,并允许重用包括PID的进程ID。如果主机的PID名称空间与容器共享,它基本上允许容器内的进程查看主机上的所有程。这就打破了主机和容器之间进程级别隔离的优点。若访问容器最终可以知道主机系统上运行的所有进程,甚至可以从容器内杀死主机系统进程。。因此不要将容器与主机的进程名称空间共享。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:PidMode={{.HostConfig.PidMode}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:PidMode=

判断方法: 如果上述命令返回host,则表示主机PID名称空间与容器共享,存在安全风险。

加固方法: 不要使用’–pid=host’参数启动容器。

操作影响: 容器进程无法看到主机系统上的进程。在某些情况下,可能需要容器共享主机的进程命名空间。可以使用像strace或gdb这样的调试工具构建容器,在调试容器中的进程时要使用这些工具。

默认值: 默认情况下,所有容器都启用了PID命名空间,并且主机的进程命名空间不与容器共享。

5.14 主机的IPC命令空间不共享

描述: IPC命名空间提供命名共享内存段,信号量和消息队列的分离。主机上的IPC命名空间不应该与容器共享,并且应该保持独立。 加固说明: IPC命名空间提供主机和容器之间的IPC分离。如果主机的IPC名称空间与容器共享,它允许容器内的进程查看主机系统上的所有IPC。这打破了主机和容器之间IPC级别隔离。可通过访问容器操纵主机IPC。因此不要将主机的IPC命名空间与容器共享。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:IpcMode={{.HostConfig.IpcMode}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:IpcMode=shareable

判断方法: 如果上述命令返回host,则意味着主机IPC命名空间与容器共享。如果上述命令不返回任何内容,则主机的IPC命名空间不会共享。

加固方法: 不要使用’–ipc=host’参数启动容器。

操作影响: 共享内存段用于加速进程间通信。它通常被高性能应用程序使用。如果这些应用程序被容器化为多个容器,则可能需要共享容器的IPC名称空间以实现高性能。在这种情况下,仍然应该共享容器特定的IPC命名空间而不是整个主机IPC命名空间。可以将容器的IPC名称空间与另一个容器共享

默认值: 默认情况下,所有容器都启用IPC命名空间,并且主机IPC命名空间不与任何容器共享。

5.15 主机设备不直接共享给容器

描述: 主机设备可以在运行时直接共享给容器。不要将主机设备直接共享给容器,特别是对不受信任的容器。 加固说明: 选项–device将主机设备共享给容器,因此容器可以直接访问这些主机设备。不允许容器以特权模式运行以访问和操作主机设备。默认情况下,容器将能够读取,写入和mknod这些设备。此外,容器可能会从主机中删除设备。因此,不要直接将主机设备共享给容器。如果必须的将主机设备共享给容器,适当地使用共享权限:r-readw -writem -mknod allowed 检测方法:

代码语言:javascript复制
docker ps -q -a |xargs docker inspect --format '{{.Id}}:Devices={{.HostConfig.Devices}}'

判断方法: 验证从容器中访问主机设备,并且正确设置所需的权限。

加固方法: 不要将主机设备直接共享于容器。如果必须将主机设备共享给容器,使用正确的一组权限:

操作影响: 将无法直接在容器内使用主机设备。

默认值: 默认情况下,主机设备不共享于容器。如果不提供共享权限并选择将主机设备展示给容器,则主机设备将具有读取写入权限。

5.16 设置默认的ulimit配置(在需要时)

描述: 默认的ulimit是在Docker守护进程级别设置的, 如果需要可以在容器运行时重写默认的ulimit设置。 加固说明: ulimit提供对shell可用资源的控制。设置系统资源控制可以防止资源耗尽带来的问题,如fork炸弹。有时候合法的用户和进程也可能过度使用系统资源,导致系统资源耗尽应该遵守在Docker守护进程级别设置的默认ulimit。如果默认的ulimit设置不适合特定的容器实例,则可以将它们覆盖为例外。但是尽量不要这样做。如果有太多例外的话,可以直接修改默认的ulimit设置。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:Ulimits={{.HostConfig.Ulimits}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:Ulimits=<no value>

判断方法: 对于每个容器实例,上述命令应该返回 Ulimits=<novalue>,除非出现异常并且需要覆盖默认的ulimit设置。

加固方法: 如果需要,在docker run或docker-compose启动参数中设定正确合理的ulimit值。

操作影响: 如果ulimits未正确设置,则可能无法实现所需的资源控制,甚至导致系统无法使用。

默认值: 容器实例继承在Docker守护进程级别设置的默认ulimit

5.17 设置主机的UTS命令空间不共享

描述: UTS命名空间提供两个系统标识符的隔离:主机名和NIS域名。 加固说明: 它于设置在该名称空间中运行进程可见的主机名和域名。在容器中运行的进程通常不需要知道主机名和域名。与主机共享UTS命名空间提供了容器可更改主机的主机名。这是不安全的,因此名称空间不应与主机共享。 检测方法:

代码语言:javascript复制
~$ docker ps -q -a | xargs docker inspect --format '{{.Id}}:UTSMode={{.HostConfig.UTSMode}}'
5d8e597549062d7709b667457e278e33f15221cb5c8e112bcbb648b3bca59f04:UTSMode=
b28b6bd4264d9aad4eff7214df6d368c44b5c252a6d61bb7fd85ebc75ffdc957:UTSMode=

判断方法: 如果上述命令返回host,则意味着主机UTS名称空间与容器共享,不符合要求。如果上述命令不返回任何内容,则主机的UTS名称空间不共享。

加固方法: 不要使用'--uts=host'参数启动容器。

操作影响: None.

默认值: 默认情况下,所有容器都启用了UTS命名空间,并且主机UTS命名空间不与任何容器共享。

备注:

5.18 不要使用docker的默认网桥docker0

描述: 不要使用Docker的默认bridge docker0。使用docker的用户定义的网络进行容器联网。 加固说明: Docker将以桥模式创建的虚拟接口连接到名为docker0的公共桥。这种默认网络模型易受ARP欺骗和MAC洪泛攻击的攻击,因为没有应用过滤。 检测方法: 运行以下命令,并验证容器是否在用户定义的网络上,而不是默认的docker0网桥。

代码语言:javascript复制
~$ docker network ls -q | xargs docker network inspect --format '{{.Name}}:{{.Options}}'
bridge:map[com.docker.network.bridge.default_bridge:true com.docker.network.bridge.enable_icc:true com.docker.network.bridge.enable_ip_masquerade:true com.docker.network.bridge.host_binding_ipv4:0.0.0.0 com.docker.network.bridge.name:docker0 com.docker.network.driver.mtu:1500]
harbor_harbor:map[]

判断方法: 检查是否在使用bridge docker0默认网桥 加固方法: 遵循Docker文档并设置用户定义的网络。运行定义的网络中的所有容器。 操作影响: 必须管理用户定义的网络。 默认值: 默认情况下,docker在其docker0桥上运行容器。

0x0n 本章总结

描述: 本章中的技巧和示例可以提高您的Docker相关知识并改善Docker镜像的安全及其质量。但是在构建Docker镜像之外,还有许多其他事情可以改善我们处理镜像和容器的方式,例如应用seccomp策略,使用cgroups或可能使用完全不同的容器运行时与引擎来限制资源消耗。

1.参考来源

Docker 安全性解决实践: https://docs.docker.com/engine/security/ CIS Docker Community Edition Benchmark

2.工具展示

  • snyk : 查找、检测、监控镜像容器安全漏洞软件平台,项目地址: https://snyk.io/
  • trivy : 镜像扫描工具,项目地址: https://github.com/trivy/trivy
  • docker-bench-security : 镜像扫描工具 ,项目地址: https://github.com/docker/docker-bench-security

0 人点赞