GVKun编程网logo

Docker 安全最佳实践和备忘单(docker的安全性)

5

在本文中,我们将带你了解Docker安全最佳实践和备忘单在这篇文章中,我们将为您详细介绍Docker安全最佳实践和备忘单的方方面面,并解答docker的安全性常见的疑惑,同时我们还将给您一些技巧,以帮

在本文中,我们将带你了解Docker 安全最佳实践和备忘单在这篇文章中,我们将为您详细介绍Docker 安全最佳实践和备忘单的方方面面,并解答docker的安全性常见的疑惑,同时我们还将给您一些技巧,以帮助您实现更有效的10 个 Docker 镜像安全最佳实践、17 条 Docker 最佳实践、AWS 云上安全最佳实践、Docker 与 Kubernetes 最佳实践 - 架构师必备技能 docker 入门到专精高阶视频教程

本文目录一览:

Docker 安全最佳实践和备忘单(docker的安全性)

Docker 安全最佳实践和备忘单(docker的安全性)

容器不是安全设备。这就是为什么我们策划了一组易于操作的建议来提高您的 Docker 容器安全性。

多年来,Docker 容器一直是开发人员工具箱的重要组成部分,使他们能够以标准化的方式构建、分发和部署他们的应用程序。

毫无疑问,这种吸引力的增加伴随着与容器化技术相关的安全问题的激增。事实上,容器也代表了攻击者的标准化攻击对象。他们可以很容易地利用错误配置并从容器内逃逸到主机。

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

这里的关键是容器默认没有任何安全维度。它们的安全性完全取决于:

支持基础设施(操作系统和平台)
容器本身包含的软件组件
运行时配置

容器安全是一个广泛的话题,但好消息是,许多最佳实践都是唾手可得的成果,可以快速减少其部署的攻击面。

这就是为什么我们在构建和运行时策划了一组关于 Docker 容器配置的最佳建议。

注意:在像 Kubernetes 这样的托管环境中,这些设置都可以被安全上下文或其他更高级别的安全规则覆盖。

构建配置

检查镜像
谨慎选择您的基础镜像docker pull image:tag

您应该始终更喜欢使用受信任的镜像,最好来自Docker 官方镜像,以减轻供应链攻击。如果您需要选择基础发行版,建议使用 Alpine Linux,因为它是可用的最轻量级发行版之一,可确保减少攻击范围。

我需要使用最新的还是固定的发行版本?

首先,您应该了解 Docker tag的工作方式从少到多,这就是为什么:python:3.9.6-alpine3.14

python:3.9.6-alpine

python:3.9-alpine

python:alpine

如上所示,基础镜像都是一样的(在撰写本文时)。

通过非常具体并确定一个版本,您可以保护自己免受未来任何重大更改的影响。另一方面,使用最新版本可确保修补更多漏洞。这是一种权衡,但通常建议固定到稳定版本。

考虑到这一点,我们会选择python:3.9-alpine这里。

注意:这同样适用于在镜像构建过程中安装包。

始终使用非特权用户
默认情况下,容器内的进程以 root (id=0)身份运行。

为了执行最小权限原则,您应该设置一个默认用户。为此,您有两个选择:

使用以下选项指定运行容器中不存在的任意用户 ID:docker run -u 4000 <image>
注意:如果您以后需要挂载文件系统,您应该将您使用的用户 ID 与主机用户相匹配,以便访问文件。

或者通过在 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选项的单独命名空间。

小心处理环境变量
你永远不应该在 ENV 指令中以明文形式包含敏感信息:它们根本不是一个安全的地方来存储你不想出现在最后一层的任何信息。例如,如果您认为像这样取消设置环境变量:

ENV $VAR

RUN unset $VAR
这不是安全的!$VAR仍然会存在于容器中,并且可以随时被获取!

为了防止运行时读取访问,请使用单个 RUN 命令在单个层中设置和取消设置变量(不要忘记变量仍然可以从镜像中提取)。

RUN export ADMIN_USER="admin" \
    && ... \
    && unset ADMIN_USER

不幸的是,密码经常被硬编码到 docker 镜像中,不过开发可以利用密码扫描引擎来查找Dockerfile 是否存在密码

不要暴露 Docker 守护进程套接字
除非你对自己正在做的事情非常有信心,否则永远不要暴露 Docker 正在侦听的 UNIX 套接字:/var/run/docker.sock

这是 Docker API 的主要入口点。授予某人访问权限等同于授予对您的主机的无限制 root 访问权限。你永远不应该将它暴露给其他容器:

-v /var/run/docker.sock://var/run/docker.sock

特权、能力和共享资源
首先,您的容器不应该以特权身份运行,否则,它将被允许在主机上拥有所有 root 权限。为了更安全,建议明确禁止在使用选项创建容器后添加新权限的可能性--security-opt=no-new-privileges。

其次,功能是 Docker 使用的一种 Linux 机制,用于将二进制root/non-root二分法转变为细粒度的访问控制系统:您的容器使用一组默认的已启用功能运行,而您很可能不这样做都需要。

建议删除所有默认功能并单独添加它们:

例如,请参阅默认功能列表,Web 服务器可能只需要 NET_BIND_SERVICE 来绑定到 1024 下的端口(如端口 80)。

第三,不要共享主机文件系统的敏感部分:

root (/),
device (/dev)
process (/proc)
virtual (/sys)挂载点

如果您需要访问主机设备,请小心使用[r|w|m]标志(读、写和使用 mknod)有选择地启用访问选项。

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

同样的想法,建议使用以下选项指定内存和 CPU 使用率

--memory=”400m”
--memory-swap=”1g”

--cpus=0.5
--restart=on-failure:5
--ulimit nofile=5
--ulimit nproc=5

参考:https://docs.docker.com/config/containers/resource_constraints/

文件系统

只允许读访问根文件系统
容器应该是短暂的,因此大多是无状态的。这就是为什么您通常可以将挂载的文件系统限制为只读的原因。docker run --read-only <image>

对非持久性数据使用临时文件系统
如果您只需要临时存储,请使用适当的选项docker run --read-only --tmpfs /tmp:rw ,noexec,nosuid <image>

将文件系统用于持久数据
如果您需要与主机文件系统或其他容器共享数据,您有两个选择:

创建具有有限可用磁盘空间的绑定安装 ( --mount type=bind,o=size)为专用分区创建绑定卷 ( --mount type=volume)

在任何一种情况下,如果容器不需要修改共享数据,请使用只读选项。

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是在启动时创建的网桥,用于将主机网络与容器网络分开。

创建容器时,Dockerdocker0默认将其连接到网络。因此,所有容器都相互连接docker0并能够相互通信。

您应该通过指定选项禁用所有容器的默认连接,--bridge=none而是使用以下命令为每个连接创建一个专用网络:

docker network create <network_name>

然后用它来访问主机网络接口

docker run --network=<network_name>

例如,要创建一个与数据库通信的 Web 服务器(在另一个容器中启动),最佳实践是创建一个桥接网络WEB以路由来自主机网络接口的传入流量,并使用另一个DB仅用于连接数据库的桥接器和网络容器。

不要共享主机的网络命名空间
同样的想法,隔离主机的网络接口:--network理论上不应使用主机选项。

日志记录
默认日志级别为 INFO,但您可以使用以下选项指定另一个级别:--log-level="debug"|"info"|"warn"|"error"|"fatal"

鲜为人知的是 Docker 的日志导出能力:如果您的容器化应用程序生成事件日志,您可以使用选项重定向STDERR和STDOUT流到外部日志服务以进行解耦--log-driver=<logging_driver>

您还可以启用双日志记录以在使用外部服务时保留 docker 对日志的访问。如果您的应用程序使用专用文件(通常写在 /var/log),您仍然可以重定向这些流:请参阅官方文档:https://docs.docker.com/config/containers/logging/configure/

扫描漏洞和密码
最后但并非最不重要的一点是,我希望您现在清楚您的容器只会与它们运行的软件一样安全。为确保您的镜像没有漏洞,您需要对已知漏洞执行扫描。许多工具可用于不同的用例和不同的形式:

漏洞扫描:

免费选项:
Clair
Trivy
Docker Bench for Security

商业的:
Snyk(提供开源和免费选项)
Anchore(提供开源和免费选项)
JFrog X 射线
Qualys

密码扫描:
ggshield(提供开源和免费选项)
SecretScanner(免费)

10 个 Docker 镜像安全最佳实践

10 个 Docker 镜像安全最佳实践

柳泉波 译 分布式实验室

图片

《Docker 镜像安全最佳实践速查表[1]》列举了 10 个诀窍和指南,确保更安全和更高质量的 Docker 镜像处理。此外,还可以检视有关 Docker 安全的新报告《Docker 安全要趁早[2]》。

图片



1. 选用最小化基础镜像

图片


人们编写项目的 Dockerfile 时,经常使用一个通用的 Docker 容器镜像作为基础,例如 From Node 。 Node 镜像实际上是以一个完整安装的 Debian Stretch 发行版为基础,这意味着构建得到的项目容器镜像将包含一个完整的操作系统。如果该项目不需要任何通用的系统库或者系统工具应用,最好不要使用完整的操作系统作为基础镜像。
Synx 发布的《开源安全报告-2019[3]》指出,Docker Hub 上流行的很多容器镜像,都用到了包含大量已知安全漏洞的基础镜像。例如,执行 docker pull node ,下载并使用 Node 镜像,相当于在应用中引入了一个包含 580 个已知漏洞的操作系统。

图片


从上图(摘自《开源安全报告-2019》)可知,Docker Hub 上最流行的 10 个镜像都包含已知的安全漏洞。选用最小化基础镜像,即只包含项目确实需要的系统工具和库的镜像,就能最小化系统的***面,确保所用操作系统是安全的。
了解更多 Docker 镜像安全的知识[4]。
2. 设定最小权限的 USER

图片


如果 Dockerfile 中没有指定 USER ,Docker 默认将会以超级用户 root 的身份运行容器,容器所属的命名空间(namespace)因此映射为 root 所有,这意味着容器有可能获取 Docker 宿主机的超级管理权限。不仅如此,以 root 用户身份运行容器,还扩大了***面,如果容器应用中存在安全漏洞,很容易造成权限提升。
在实践中,一般不需要容器拥有 root 权限。为了尽量降低安全威胁,创建专门的用户和用户组,在 Dockerfile 中使用 USER 指定用户,确保以最小权限的用户身份运行容器应用。
如果基础镜像中不包含专门的用户,那么就在 Dockerfile 中直接创建。下面就是一个这样的例子,它用到的基础镜像是 Ubuntu :

image.png

在上例中:

  • 创建一个系统用户( -r 选项),没有密码、没有主目录且没有 shell;

  • 将该用户添加到前面(使用 groupadd )创建的用户组;

  • 最后一段参数设定了用户名以及所属的用户组。


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

FROM node:10-alpine RUN mkdir /appcopY . /appRUN chown -R node:node /appUSER nodeCMD [“node”, “index.js”]


Node.js 应用开发者请参阅官方的Docker 和 Node.js 最佳实践[5]。
3. 签名和校验镜像,防范中间人***

图片


Docker 镜像的认证颇具挑战性。在生产环境使用这些镜像运行我们的代码,意味着我们对这些镜像的极大信任。因此,必须保证我们拉取的容器镜像确实是发布者发布的镜像,没有被任何人篡改。发生镜像篡改,有可能是因为 Docker 客户端和镜像中心之间的中间人***,或者是发布者的身份被人盗用并在镜像中心发布了恶意镜像。
校验 Docker 镜像
Docker 默认直接拉取容器镜像,不会校验镜像的来源和发布者。这意味着你有可能使用来源和发布者不明的任何镜像。无论采用何种策略,最佳实践都是先校验容器镜像,通过验证后再拉取镜像。为了体验镜像校验功能,执行下列暂时开启 Docker Content Trust 的命令:

export DOCKER_CONTENT_TRUST=1


现在,尝试拉取一个没有签名的容器镜像——请求会被拒绝,不会拉取镜像。
签名 Docker 镜像
优先使用 Docker 认证的镜像,即这些镜像来自经过 Docker Hub 检查和选择的可信提供者。不要使用无法检验来源和发布者的容器镜像。
Docker 支持镜像签名,提供了额外一层的保护。使用 Docker Notary 签名镜像。Notary 会检验镜像的签名,如果签名不合法,它会阻止运行该镜像。
如果开启了 Docker Content Trust ,构建 Docker 镜像的同时也会对镜像签名。如果是第一次签名,Docker 会为当前用户生成一个私钥,保存在 ~/docker/trust 。后续所有的镜像都会使用这个私钥签名。
请参考Docker 官方文档[6],了解签名镜像的详细指令。
4. 找出、修正和监控开源漏洞

图片


指定容器的基础镜像,同时也引入了该镜像包含的操作系统及系统库有可能存在的所有安全风险。
最好选用能够正常运行应用代码的最小化镜像,这有助于减少***面,因为限制了可能的安全漏洞数量。不过,这么做并没有对镜像进行安全审计,也不能防范将来发现的新漏洞。
因此,防范安全软件漏洞的一种方法是使用像 Snyk 这样的工具,持续扫描和监控 Docker 镜像各层可能存在的漏洞。
使用下列命令扫描容器镜像,检查是否存在已知漏洞:

image.png



Snyk 能够监控指定的容器镜像,一旦有新发现的安全漏洞,通知用户并给出修补建议:

$ snyk monitor --docker node:10


根据 Snyk 用户执行的镜像扫描,我们发现大约 40% 的 Docker 镜像包含已知漏洞,实际上弥补这些漏洞的新版本基础镜像已经有了。 Synx 提供了绝无仅有的修正建议功能,用户可以根据建议采取行动,升级 Docker 镜像。
Snyk 还发现在扫描的所有镜像中,为了减少漏洞的数量,大约 20% 的镜像需要重新构建。更多信息请参阅《开源安全报告-2019[3]》。
5. 不要在容器镜像中包含机密信息

图片


有时候,构建包含应用的容器镜像时,需要用到一些机密信息,例如从私有仓库拉取代码所需的 SSH 私钥,或者安全私有软件包所需的令牌。如果 Dockerfile 中包含复制机密信息的命令,构建镜像时,这行命令对应的中间容器会被缓存,导致机密数据也被缓存,有可能造成机密信息泄漏。因此,像令牌和密钥这样的机密信息必须保存在 Dockerfile 之外。
使用多阶段构建
利用 Docker 的多阶段构建功能,用一个中间镜像层获取和管理机密信息,然后清除中间镜像,这样在应用镜像构建阶段不涉及敏感数据。如下面例子所示,使用代码将机密信息添加到中间层:

image.png


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

image.png


想了解有关 Docker secret 的更多信息,请访问 Docker 官方站点[6]。
避免无意中复制机密信息
往镜像中复制文件时,也要当心,避免无意中添加了机密信息。例如,下面的命令将整个构建上下文文件夹复制到 Docker 镜像,有可能把敏感文件也复制进去了:

copY . .


如果文件夹中有敏感文件,要么先移除这些文件,要么将这些文件包含在 .dockerignore 中,复制时会忽略这些文件:

private.keyappsettings.json


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

图片


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

  • 优先选用最详细的镜像标签。例如,镜像有 :8、:8.0.1 和 :8.0.1-alpine 等标签,选择最后这个,因为它提供了最详细的信息。不要使用像 latest 这样过于泛泛的标签。

  • 记住,镜像的发布者有可能删除镜像的某个标签。如果设定了所用镜像的标签,一旦这个标签被删除,镜像构建会因为找不到基础镜像而失败。为了避免这个问题,可以提前把该镜像复制到私有镜像中心或者公有镜像中心的私人账户下面。这么做,保证了镜像的不可更改性,同时也带来了维护私有镜像中心的负担。

  • 使用比签名更具体的 SHA256 引用指明要使用的镜像,这能保证每次拉取都是相同内容的镜像。这么做也有风险,如果镜像改变了,以前的 SHA256 引用(散列值)也不存在了。


7. 使用 copY ,不要使用 ADD

图片


译者警告:这部分对 ADD 和 copY 的描述,与 Docker 官方文档并不吻合,译者按照自己的理解修改了这部分内容。如果要了解作者原意,请阅读英文原文[7]。
从宿主机复制文件到容器镜像中的 Docker 命令有两个:copY 和 ADD ,这两个命令本质上很相似,但具体功能并不相同:

  • copY - 将本地文件或者目录(递归)复制到容器镜像中的目标目录,复制来源和目标都必须明确指定。

  • ADD - 与 copY 类似的功能,有两个不同:(1)如果复制来源是本地压缩文件,ADD 将把该文件解压缩到目标目录;(2)ADD 也可以将远程 URL 指定的文件下载到目标目录。


为了避免可能导致的安全问题,请记住 copY 和 ADD 的不同:

  • 使用 ADD 从远程 URL 下载文件,存在中间人***的风险,文件内容有可能因此被篡改。必须确保远程 URL 必须是安全的 TLS 链接,校验远程 URL 的来源和身份。译者注:实际上,官方文档并不鼓励使用 ADD 添加远程文件。

  • 如果复制的是本地压缩文件, ADD 自动将它解压缩到目标目录,这有可能触发 zip 炸弹或者 zip 任意文件覆盖漏洞。

  • 相比较而言,使用 copY 复制文件或目录,会创建一个缓存的中间镜像层,优化镜像构建的速度。


8. 使用 LABEL 指定镜像元数据

图片


镜像元数据有助于用户更好地理解和使用该镜像。最常见的元数据是 maintainer ,它说明了镜像维护者的电邮地址和名字。使用 LABEL 命令添加镜像的元数据:

LABEL maintainer="me@acme.com"


除了镜像的维护者信息,添加其他你认为重要的元数据,包括提交对象的散列值、相关构建的链接、质量状态(通过所有测试了吗?)、源代码链接、Security.TXT 文件的位置等。
Security.TXT (RFC5785)[8] 文件说明了镜像维护者的安全披露政策。最好在镜像元数据中加上 Security.TXT 的链接,例如:

LABEL securitytxt="https://www.example.com/.well-kNown/security.txt"


想了解镜像元数据的更多信息,请访问 https://label-schema.org/rc1/ 。
译者注:这个规范好像已经废止了,请直接访问 OCI 镜像规范[9]。
9. 使用多阶段构建小而安全的镜像

图片


使用 Dockerfile 构建应用容器镜像时,会生成很多只是构建时需要的镜像层,包括编译时所需的开发工具和库,运行单元测试所需的依赖、临时文件、机密信息等等。
如果保留这些镜像层,不仅会增加镜像的大小,影响镜像下载速度,而且会因为安装更多软件包而面临更大的***危险。这对用到的镜像也是成立的——需要使用一个专门构建应用的镜像,但不会用它来运行应用代码。
Go 语言就是一个很好的例子。构建一个 Go 应用需要用到 Go 编译器。编译得到的 Go 应用能够在任何操作系统上直接运行,没有任何依赖,包括 scratch 镜像。
Docker 因此提供了多阶段构建的功能,允许在构建过程中使用多个临时镜像,只保留最后一个镜像。这样,用户得到两个镜像:

  • 第一个镜像——非常大的镜像,包含了构建应用和运行测试所需的所有依赖;

  • 第二个镜像——非常小的镜像,只包含运行应用所需的极少数依赖。


10. 使用静态分析工具

图片


使用静态分析工具,能够避免常见的错误,建立工程师自动遵循的最佳实践指南。
例如,hadolint 分析 Dockerfile 并列出不符合最佳实践规则的地方。

图片


在集成开发环境(IDE)中使用 hadolint 更好。例如,安装 VS Code 的 hadolint 扩展后,编写 Dockerfile 时,边写边检查,既快又好。
了解更多 Docker 镜像安全的知识[10]。
把速查表打印出来,贴在某处,时刻提醒自己构建容器镜像时应该遵循的最佳安全实践!
相关链接:

  1. https://res.cloudinary.com/snyk/image/upload/v1551798390/Docker_Image_Security_Best_Practices_.pdf

  2. https://snyk.io/blog/shifting-docker-security-left/

  3. https://snyk.io/blog/top-ten-most-popular-docker-images-each-contain-at-least-30-vulnerabilities/

  4. https://snyk.io/container-vulnerability-management/

  5. https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md

  6. https://docs.docker.com/engine/security/trust/content_trust/

  7. https://snyk.io/blog/10-docker-image-security-best-practices/

  8. https://securitytxt.org/

  9. https://github.com/opencontainers/image-spec

  10. https://snyk.io/container-vulnerability-management/


原文链接:https://snyk.io/blog/10-docker-image-security-best-practices/

17 条 Docker 最佳实践

17 条 Docker 最佳实践

本篇分享在编写 Dockerfiles 和使用 Docker 时应遵循的一些最佳实践。篇幅较长,建议先收藏慢慢看,保证看完会很有收获。

文章目录

Dockerfile 最佳实践

  1. 使用多阶段的构建

  2. 调整 Dockerfile 命令的顺序

  3. 使用小型 Docker 基础镜像

  4. 尽量减少层的数量

  5. 使用无特权的容器

  6. 优先选择 COPY 而不是 ADD

  7.  Python 包缓存到 Docker 主机上

  8. 每个容器只运行一个进程

  9. 优先选择数组而不是字符串语法

  10. 理解 ENTRYPOINT  CMD 之间的区别

  11. 添加健康检查 HEALTHCHECK

Docker 镜像最佳实践

  1. Docker 镜像的版本

  2. 不要在图像中存储密钥

  3. 使用 .dockerignore 文件

  4. 检查和扫描你的 Docker 文件和镜像

  5. 签署和验证图像

  6. 设置内存和 CPU 的限制

Dockerfile 最佳实践

1. 使用多阶段的构建

利用多阶段构建的优势来创建更精简、更安全的 Docker 镜像。多阶段 Docker 构建 (multi-stage builds[1]) 允许你将你的 Dockerfile 分成几个阶段。

例如,你可以有一个阶段用于编译和构建你的应用程序,然后可以复制到后续阶段。由于只有最后一个阶段被用来创建镜像,与构建应用程序相关的依赖关系和工具就会被丢弃,因此可以留下一个精简的、模块化的、可用于生产的镜像。

Web 开发示例:

# 临时阶段
FROM python:3.9-slim as builder

WORKDIR /app

RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc

COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt


# 最终阶段
FROM python:3.9-slim

WORKDIR /app

COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .

RUN pip install --no-cache /wheels/*

在这个例子中,GCC 编译器在安装某些 Python 包时是必需的,所以我们添加了一个临时的、构建时的阶段来处理构建阶段。

由于最终的运行时映像不包含 GCC,所以它更轻,也更安全。镜像大小比较:

REPOSITORY                 TAG                    IMAGE ID       CREATED          SIZE
docker-single              latest                 8d6b6a4d7fb6   16 seconds ago   259MB
docker-multi               latest                 813c2fa9b114   3 minutes ago    156MB

再来看一个例子:

# 临时阶段
FROM python:3.9 as builder

RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels jupyter pandas


# 最终阶段
FROM python:3.9-slim

WORKDIR /notebooks

COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/*

镜像大小比较:

REPOSITORY                  TAG                   IMAGE ID       CREATED         SIZE
ds-multi                    latest                b4195deac742   2 minutes ago   357MB
ds-single                   latest                7c23c43aeda6   6 minutes ago   969MB

总之,多阶段构建可以减少你的生产镜像的大小,帮助你节省时间和金钱。此外,这将简化你的生产容器。由于尺寸较小和简单,相对会有较小的攻击面。

2. 调整 Dockerfile 命令的顺序

密切注意你的 Dockerfile 命令的顺序,以利用层缓存。

Docker 在一个特定的 Docker 文件中缓存每个步骤(或层),以加快后续的构建。当一个步骤发生变化时,不仅该步骤,而且所有后续步骤的缓存都将被废止。

例如:

FROM python:3.9-slim

WORKDIR /app

COPY sample.py .

COPY requirements.txt .

RUN pip install -r /requirements.txt

在这个 Dockerfile 中,我们在安装需求之前复制了应用程序的代码。现在,每次我们改变 sample.py 时,构建都会重新安装软件包。这是非常低效的,特别是在使用 Docker 容器作为开发环境时。因此,把经常变化的文件放在 Dockerfile 的末尾是很关键的。

你也可以通过使用 .dockerignore 文件来排除不必要的文件,使其不被添加到 Docker 构建环境和最终镜像中,从而帮助防止不必要的缓存失效。更多信息后面会提到。

因此,在上面的 Dockerfile 中,你应该把 COPY sample.py . 命令移到底部,如下所示:

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install -r /requirements.txt

COPY sample.py .

注意。

  1. 总是把可能发生变化的层放在 Dockerfile 中尽可能的低。

  2. 将多个 RUN apt-get updateRUN apt-get install 等命令结合到一起执行。(这也有助于减少镜像的大小,后面会很快就会提到这一点)。

  3. 如果你想关闭某个 Docker 构建的缓存,可以添加 --no-cache=True 标志。

3. 使用小型 Docker 基础镜像

较小的 Docker 镜像更具有模块化和安全性。较小的 Docker 基础镜像在构建、推送和拉动镜像的速度较小,它们也往往更安全,因为它们只包括运行应用程序所需的必要库和系统依赖。

你应该使用哪个 Docker 基础镜像?这个没有一个固定的答案,它这取决于你要做什么。下面是 Python 的各种 Docker 基础镜像的大小比较。

REPOSITORY   TAG                 IMAGE ID       CREATED      SIZE
python       3.9.6-alpine3.14    f773016f760e   3 days ago   45.1MB
python       3.9.6-slim          907fc13ca8e7   3 days ago   115MB
python       3.9.6-slim-buster   907fc13ca8e7   3 days ago   115MB
python       3.9.6               cba42c28d9b8   3 days ago   886MB
python       3.9.6-buster        cba42c28d9b8   3 days ago   886MB

虽然基于 Alpine Linux 的 Alpine flavor 是最小的,但如果你找不到可以与之配合的编译二进制文件,往往会导致构建时间的增加。因此,你最终可能不得不自己构建二进制文件,这可能会增加图像的大小(取决于所需的系统级依赖)和构建时间(由于必须从源头编译)。

关于为什么最好不要使用基于 Alpine 的基础镜像,请参考适用于 Python 应用程序的最佳 Docker 基础映像 [2]  使用 Alpine 可以使 Python Docker 构建速度慢 50 倍 [3] 了解更多关于为什么最好避免使用基于 Alpine 的基础图像。

归根结底,这都是关于平衡的问题。如果有疑问,从 *-slim flavor 开始,特别是在开发模式下,因为你正在构建你的应用程序。你想避免在添加新的 Python 包时不得不不断地更新 Dockerfile 以安装必要的系统级依赖。当你为生产强化你的应用程序和 Dockerfile 时,你可能想探索使用 Alpine 来完成多阶段构建的最终镜像。

另外,别忘了定期更新你的基础镜像,以提高安全性和性能。当一个基础镜像的新版本发布时,例如:3.9.6-slim --> 3.9.7-slim,你应该拉出新的镜像并更新你正在运行的容器以获得所有最新的安全补丁。

4. 尽量减少层的数量

尽量把 RUNCOPY  ADD 命令结合起来使用,因为它们会创建层。每一层都会增加图像的大小,因为它们是被缓存的。因此,随着层数的增加,镜像大小也会增加。

你可以用 docker history 命令来测试一下。

docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
dockerfile   latest    180f98132d02   51 seconds ago   259MB

docker history 180f98132d02

IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
180f98132d02   58 seconds ago       COPY . . # buildkit                             6.71kB    buildkit.dockerfile.v0
<missing>      58 seconds ago       RUN /bin/sh -c pip install -r requirements.t…   35.5MB    buildkit.dockerfile.v0
<missing>      About a minute ago   COPY requirements.txt . # buildkit              58B       buildkit.dockerfile.v0
<missing>      About a minute ago   WORKDIR /app
...

请注意尺寸。只有 RUNCOPY  ADD 命令增加了图像的尺寸,你可以尽可能地通过合并命令来减少图像的大小。比如:

RUN apt-get update
RUN apt-get install -y gcc

可以合并成一个 RUN 命令:

RUN apt-get update && apt-get install -y gcc

因此,创建一个单层而不是两个,这就减少了最终图像的大小。虽然减少层数是个好主意,但更重要的是,这本身不是一个目标,而是减少镜像大小和构建时间的一个副作用。换句话说呢,与其试图优化每一条命令,你更应该关注前面的三种做法!!!

  1. 多阶段构建

  2. Dockerfile 命令的顺序

  3. 以及使用一个小的基础镜像。

注意

  1. RUNCOPY  ADD 都会创建图层

  2. 每个图层都包含与前一个图层的差异

  3. 图层会增加最终图像的大小

提示

  1. 合并相关命令

  2. 在创建过程中执行 RUN 步骤中删除不必要的文件

  3. 尽量减少运行 apt-get upgrade 的次数,因为它将所有软件包升级到最新版本。

  4. 对于多阶段的构建,不要太担心过度优化临时阶段的命令

最后,为了便于阅读,建议将多行参数按字母数字排序。

RUN apt-get update && apt-get install -y \
    git \
    gcc \
    matplotlib \
    pillow  \
    && rm -rf /var/lib/apt/lists/*

5. 使用无特权的容器

默认情况下,Docker 在容器内以 root 身份运行容器进程。然而,这是一个糟糕的做法,因为在容器内以 root 身份运行的进程在 Docker 主机中也是以 root 身份运行。

因此,如果攻击者获得了对容器的访问权,他们就可以获得所有的 root 权限,并可以对 Docker 主机进行一些攻击,例如:

  1. 将敏感信息从主机的文件系统复制到容器中

  2. 执行远程命令

为了防止这种情况,确保以非 root 用户运行容器进程。

RUN addgroup --system app && adduser --system --group app

USER app

你可以更进一步,删除 shell 权限,确保没有主目录。

RUN addgroup --gid 1001 --system app && \
    adduser --no-create-home --shell /bin/false --disabled-password --uid 1001 --system --group app

USER app

验证

docker run -i sample id

uid=1001(app) gid=1001(app) groups=1001(app)

在这里,容器内的应用程序在一个非 root 用户下运行。然而,请记住,Docker 守护进程和容器本身仍然是以 root 权限运行的。

请务必查看以非根用户身份运行 Docker 守护进程,以获得以非根用户身份运行守护进程和容器的帮助。

6. 优先选择 COPY 而不是 ADD

除非你确定你需要 ADD 所带来的额外功能,否则请使用 COPY

那么 COPY  ADD 的区别是什么?

首先,这两个命令都允许你从一个特定的位置复制文件到 Docker 镜像中。

ADD <src> <dest>
COPY <src> <dest>

虽然它们看起来作用相同,但 ADD 有一些额外的功能。

  • COPY 用于将本地文件或目录从 Docker 主机复制到镜像上。

  • ADD 可以用于同样的事情,也可以用于下载外部文件。另外,如果你使用压缩文件(tar、gzip、bzip2 等)作为参数,ADD 会自动将内容解压到指定位置。

# 将主机上的本地文件复制到目的地
COPY /source/path  /destination/path
ADD /source/path  /destination/path

# 下载外部文件并复制到目的地
ADD http://external.file/url  /destination/path

# 复制和提取本地压缩文件
ADD source.file.tar.gz /destination/path

最后 COPY 在语义上比 ADD 更加明确和更容易理解。

7. 缓存安装包到 Docker 主机上

当一个需求文件被改变时,镜像需要被重建以安装新的包。先前的步骤将被缓存,正如在最小化层数中提到的。在重建镜像时下载所有的包会导致大量的网络活动,并需要大量的时间。每次重建都要占用同等的时间来下载不同构建中的通用包。

以 Python 为例,你可以通过将 pip 缓存目录映射到主机上的一个目录来避免这种情况。所以对于每次重建,缓存的版本会持续存在,这可以提高构建速度。

在 Docker 运行中添加一个卷,作为 -v $HOME/.cache/pip-docker/:/root/.cache/pip 或者作为 Docker Compose 文件中的映射。

上面介绍的目录只供参考,要确保你映射的是 cache 目录,而不是 site-packages(内置包所在的位置)。

将缓存从 docker 镜像中移到主机上可以为你节省最终镜像的空间。

# 忽略 ...

COPY requirements.txt .

RUN --mount=type=cache,target=/root/.cache/pip \
        pip install -r requirements.txt

# 忽略 ...

8. 每个容器只运行一个进程

为什么建议每个容器只运行一个进程?

让我们假设你的应用程序栈由两个 Web 服务器和一个数据库组成。虽然你可以很容易地从一个容器中运行所有三个,但你应该在一个单独的容器中运行每个服务,以便更容易重复使用和扩展每个单独的服务。

  • 扩展性 - 由于每个服务都在一个单独的容器中,你可以根据需要水平地扩展你的一个网络服务器来处理更多的流量。

  • 可重用性 - 也许你有另一个服务需要一个容器化的数据库,你可以简单地重复使用同一个数据库容器,而不需要带着两个不必要的服务。

  • 日志 - 耦合容器会让日志变得更加复杂。(我们将在本文后面进一步详细讨论这个问题)

  • 可移植性和可预测性 - 当容器有较少的部分在工作时,制作安全补丁或调试问题就会容易得多。

9. 优先选择数组而不是字符串语法

你可以在你的 Dockerfiles 中以数组(exec)或字符串(shell)格式

在 Dockerfile 中,你可以以数组(exec)或字符串(shell)格式来使用 CMD  ENTRYPOINT 命令

# 数组(exec)
CMD ["gunicorn""-w""4""-k""uvicorn.workers.UvicornWorker""main:app"]

# 字符串(shell)
CMD "gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app"

两者都是正确的,并且实现了几乎相同的事情;但是,你应该尽可能地使用 exec 格式。

以下来自 Docker 的官方文档 [4] 内容:

  • 确保你在 Dockerfile 中使用 CMD  ENTRYPOINT 的 exec 形式。

  • 例如,使用 ["program", "arg1", "arg2"] 而不是 "program arg1 arg2"。使用字符串形式会导致 Docker 使用 bash 运行你的进程,而 bash 并不能正确处理信号。Compose 总是使用 JSON 形式,所以不用担心如果你在你的 Compose 文件中覆盖了命令或入口。

因此,由于大多数 shell 不处理对子进程的信号,如果你使用 shell 格式,CTRL-C(产生 SIGTERM)可能不会停止一个子进程。

例子:

FROM ubuntu:18.04

# BAD: 字符串(shell)格式
ENTRYPOINT top -d

# GOOD: 数组(exec)格式
ENTRYPOINT ["top""-d"]

这两种情况执行效果一样。但请注意,在字符串(shell)格式的情况下,CTRL-C 不会杀死这个进程。相反,你会看到 ^C^C^C^C^C^C^C^C^C^C

另一个注意事项是,字符串(shell)格式携带的是 shell 的 PID,而不是进程本身。

# 数组格式
root@18d8fd3fd4d2:/app# ps ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 python manage.py runserver 0.0.0.0:8000
    7 ?        Sl     0:02 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
   25 pts/0    Ss     0:00 bash
  356 pts/0    R+     0:00 ps ax


# 字符串格式
root@ede24a5ef536:/app# ps ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 /bin/sh -c python manage.py runserver 0.0.0.0:8000
    8 ?        S      0:00 python manage.py runserver 0.0.0.0:8000
    9 ?        Sl     0:01 /usr/local/bin/python manage.py runserver 0.0.0.0:8000
   13 pts/0    Ss     0:00 bash
  342 pts/0    R+     0:00 ps ax

10. 了解 ENTRYPOINT  CMD 之间的区别

我应该使用 ENTRYPOINT 还是 CMD 来运行容器进程?有两种方法可以在容器中运行命令。

CMD ["gunicorn""config.wsgi""-b""0.0.0.0:8000"]

# 和

ENTRYPOINT ["gunicorn""config.wsgi""-b""0.0.0.0:8000"]

两者本质上做的是同一件事:用 Gunicorn 服务器在 config.wsgi 启动应用程序,并将其绑定到 0.0.0.0:8000

CMD 很容易被重写。如果你运行 docker run <image_name> uvicorn config.asgi,上述 CMD 就会被新的参数所取代。

例如,uvicorn config.asgi。而要覆盖 ENTRYPOINT 命令,必须指定 --entrypoint 选项。

docker run --entrypoint uvicorn config.asgi <image_name>

在这里,很明显,我们正在覆盖入口点。所以,建议使用 ENTRYPOINT 而不是 CMD,以防止意外地覆盖命令。

它们也可以一起使用。比如说

ENTRYPOINT ["gunicorn""config.wsgi""-w"]
CMD ["4"]

当像这样一起使用时,为启动容器所运行的命令就变成了:

gunicorn config.wsgi -w 4

如上所述,CMD 很容易被重写。因此,CMD 可以被用来向 ENTRYPOINT 命令传递参数。比如很容易更改 workers 的数量,就像这样:

docker run <image_name> 6

这样就将有 6 个 Gunicorn workers 启动容器,而不是默认的 4 个。

11. 添加健康检查 HEALTHCHECK

使用 HEALTHCHECK 来确定容器中运行的进程是否不仅已启动并正在运行,而且是 “健康” 的。

Docker 公开了一个 API 来检查容器中运行的进程的状态,它提供的信息不仅仅是进程是否 “正在运行”,因为 “运行” 涵盖了 “它正在运行”、“仍在启动”、甚至 “陷入某种无限循环错误状态”。你可以通过 `HEALTHCHECK`[5] 指令与此 API 交互。

例如,如果你正在提供 Web 应用程序,则可以使用以下内容来确定 / 端点是否已启动并可以处理服务请求:

HEALTHCHECK CMD curl --fail http://localhost:8000 || exit 1

如果你运行 docker ps,你可以看到 HEALTHCHECK 的状态。

健康的示例

CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                            PORTS                                       NAMES
09c2eb4970d4   healthcheck   "python manage.py ru…"   10 seconds ago   Up 8 seconds (health: starting)   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   xenodochial_clarke

不健康的示例

CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS                          PORTS                                       NAMES
09c2eb4970d4   healthcheck   "python manage.py ru…"   About a minute ago   Up About a minute (unhealthy)   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   xenodochial_clarke

你可以更进一步,设置一个仅用于健康检查的自定义端点,然后配置 HEALTHCHECK 以针对返回的数据进行测试。

例如,如果端点返回 {"ping": "pong"} 的 JSON 响应,你可以指示 HEALTHCHECK 验证响应正文。

以下是使用 docker inspect 查看运行状况检查状态的方法:

这里省略了部分输出。

❯ docker inspect --format "{{json .State.Health }}" ab94f2ac7889
{
  "Status""healthy",
  "FailingStreak"0,
  "Log": [
    {
      "Start""2021-09-28T15:22:57.5764644Z",
      "End""2021-09-28T15:22:57.7825527Z",
      "ExitCode"0,
      "Output""..."

你还可以向 Docker Compose 文件添加运行状况检查:

version: "3.8"

services:
  web:
    build: .
    ports:
      - ''8000:8000''
    healthcheck:
      test: curl --fail http://localhost:8000 || exit 1
      interval: 10s
      timeout: 10s
      start_period: 10s
      retries: 3

选项:

  • test:要测试的命令。

  • interval:要测试的间隔 - 即,测试每 x 时间单位。

  • timeout:等待响应的最长时间。

  • start_period:何时开始健康检查。它可以在容器准备就绪之前执行其他任务时使用,例如运行迁移。

  • retries:在将测试指定为失败之前的最大重试次数。

如果你使用的是 Docker Swarm 以外的编排工具(比如 Kubernetes 或 AWS ECS),它们很可能有自己的内部系统来处理健康检查。在添加 HEALTHCHECK 指令之前,请参阅特定工具的文档。

Docker 镜像最佳实践

1. Docker 镜像版本

只要有可能,就要避免使用 latest 标签的镜像。

如果你依赖 latest 标签(这并不是一个真正的 "标签",因为当图像没有明确的标签时,它是默认应用的),你无法根据镜像标签来判断你的代码正在运行哪个版本。

如果你想回滚就变得很困难,并且很容易被覆盖(无论是意外还是恶意的)。标签,就像你的基础设施和部署,应该是不可改变的。

所以无论你如何对待你的内部镜像,都不应该对基本镜像使用 latest 标签,因为你可能会无意中把一个带有破坏性变化的新版本部署到生产中。

对于内部镜像,应使用描述性的标签,以便更容易分辨哪个版本的代码正在运行,处理回滚,并避免命名冲突。例如,你可以使用以下描述符来组成一个标签。

  1. 时间戳

  2. Docker 镜像 ID

  3. Git 提交哈希值

  4. 语义版本 (Semantic version)

关于更多的选择,也可以参考 Stack Overflow 问题 [6] "Properly Versioning Docker Images" 中的这个答案。

比如说

docker build -t web-prod-b25a262-1.0.0 .

在这里,我们用下面的内容来形成标签

  1. 项目名称:web

  2. 环境名称: prod

  3. Git commit short hash: b25a262 (通过命令 git rev-parse --short HEAD 来获得)

  4. 语义学版本:1.0.0

选择一个标签方案并与之保持一致是至关重要的。由于提交哈希值(commit hashes)可以很容易地将镜像标签与代码联系起来,建议将它们纳入你的标签方案。

2. 不要在镜像中存储机密信息

Secrets 是敏感的信息,如密码、数据库凭证、SSH 密钥、令牌和 TLS 证书等。这些信息不应该在没有加密的情况下被放入你的镜像中,因为未经授权的用户如果获得了镜像的访问权,只需要检查这些层就可以提取密钥。

因此不要在 Docker 文件中添加明文的密钥,尤其是当你把镜像推送到像 Docker Hub 这样的公共仓库!!

FROM python:3.9-slim

ENV DATABASE_PASSWORD "SuperSecretSauce"

相反,它们应该通过以下方式注入

  1. 环境变量(在运行时)

  2. 构建时参数(在构建时)

  3. 协调工具,如 Docker Swarm(通过 Docker secrets)或 Kubernetes(通过 Kubernetes secrets)。

此外,你还可以通过在你的 .dockerignore 文件中添加常见的密钥文件和文件夹来帮助防止密钥的泄露。

**/.env
**/.aws
**/.ssh

最后,要明确哪些文件会被复制到镜像中,而不是递归地复制所有文件。

# 不好的做法
COPY . .

# 好的做法
COPY ./app.py .

明确的做法也有助于限制缓存的破坏。

环境变量

你可以通过环境变量来传递密钥,但它们会在所有子进程、链接的容器和日志以及 docker inspect 中可见。要更新它们也很困难。

docker run --detach --env "DATABASE_PASSWORD=SuperSecretSauce" python:3.9-slim

b25a262f870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239

docker inspect --format=''{{range .Config.Env}}{{println .}}{{end}}'' b25a262f870eb0fdbf03c666e7fcf18f9664314b79ad58bc7618ea3445e39239

DATABASE_PASSWORD=SuperSecretSauce
PATH=/usr/local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
python_version=3.9.7
python_pip_version=21.2.4
python_setuptools_version=57.5.0
python_get_pip_url=https://github.com/pypa/get-pip/raw/c20b0cfd643cd4a19246ccf204e2997af70f6b21/public/get-pip.py
PYTHON_GET_PIP_SHA256=fa6f3fb93cce234cd4e8dd2beb54a51ab9c247653b52855a48dd44e6b21ff28b

这是最直接的密钥管理方法。虽然它不是最安全的,但它会让诚实的人保持诚实,因为它提供了一个薄薄的保护层,有助于使密钥不被好奇的游荡的眼睛发现。

使用共享卷传递密钥是一个更好的解决方案,但它们应该被加密,通过 Vault 或 AWS 密钥管理服务(KMS),因为它们被保存到磁盘。

构建时参数

你可以在构建时使用构建时参数来传递密钥,但这些密钥对于那些可以通过 docker 历史访问镜像的人来说是可见的。

例子

FROM python:3.9-slim


ARG DATABASE_PASSWORD

构建

docker build --build-arg "DATABASE_PASSWORD=SuperSecretSauce" .

如果你只需要临时使用密钥作为构建的一部分。例如,用于克隆私有 repo 或下载私有软件包的 SSH 密钥。你应该使用多阶段构建,因为构建者的历史会被临时阶段忽略。

# 临时阶段
FROM python:3.9-slim as builder

# 密钥参数
arg ssh_private_key

# 安装 git
RUN apt-get update && (运行 apt-get update)。
    apt-get install -y --no-install-recommends git

# 使用 ssh 密钥来克隆 repo
RUN mkdir -p /root/.ssh/ && \\
    echo "${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa
RUN touch /root/.ssh/known_hosts & &
    ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts
RUN git clone git@github.com:testdrivenio/not-real.git


# 最后阶段
FROM python:3.9-slim

工作目录 /app

# 从临时镜像中复制版本库
COPY --from=builder /your-repo /app/your-repo

多阶段构建只保留了最终镜像的历史。你可以把这个功能用于你的应用程序需要的永久密钥,比如数据库凭证。

你也可以使用 docker build 中新的 --secret 选项来向 Docker 镜像传递密钥,这些密钥不会被存储在镜像中。

"docker_is_awesome" > secrets.txt

FROM alpine

# 从默认的密钥位置显示密钥。
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret

这将装载 secrets.txt 文件中的密钥。

构建镜像

docker build --no-cache --progress=plain --secret id=mysecret,src=secrets.txt .

# 输出
...
#4 [1/2] FROM docker.io/library/alpine
#4 sha256:665ba8b2cdc0cb0200e2a42a6b3c0f8f684089f4cd1b81494fbb9805879120f7
#4 缓存的

#5 [2/2] RUN --mount=type=secret,id=mysecret cat /run/secrets/myecret
#5 sha256:75601a522ebe80ada66dedd9dd86772ca932d30d7e1b11bba94c04aa55c237de
#5 0.635 docker_is_awesome#5 DONE 0.7s

#6 导出到图像

最后,检查历史记录,看看密钥是否泄露了。

❯ docker history 49574a19241c
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
49574a19241c   5 minutes ago   CMD ["/bin/sh"]                                 0B        buildkit.dockerfile.v0
<missing>      5 minutes ago   RUN /bin/sh -c cat /run/secrets/mysecret # b…   0B        buildkit.dockerfile.v0
<missing>      4 weeks ago     /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      4 weeks ago     /bin/sh -c #(nop) ADD file:aad4290d27580cc1a…   5.6MB

Docker 密钥

如果你正在使用 Docker Swarm,你可以用 Docker secrets 来管理密钥。

例如,启动 Docker Swarm 模式。

docker swarm init

创建一个 docker 密钥。

echo "supersecretpassword" | docker secret create postgres_password -
qdqmbpizeef0lfhyttxqfbty0

docker secret ls
ID                          NAME                DRIVER    CREATED         UPDATED
qdqmbpizeef0lfhyttxqfbty0   postgres_password             4 seconds ago   4 seconds ago

当一个容器被赋予上述密钥的访问权时,它将挂载在 /run/secrets/postgres_password。这个文件将包含明文的密钥的实际值。

使用其他的编排工具?

  • 使用 AWS Secrets Manager 的密钥与 Kubernetes 的密钥 [7]

  • DigitalOcean Kubernetes - 保护 DigitalOcean Kubernetes 集群的推荐步骤 [8]

  • Google Kubernetes 引擎 - 与其他产品一起使用密钥管理器 [9]

  • Nomad - Vault 集成和检索动态密钥 [10]

3. 使用 .dockerignore 文件

之前已经提到过几次使用 .dockerignore 文件。这个文件用来指定你不希望被添加到发送给 Docker 守护进程的初始构建上下文中的文件和文件夹,后者将构建你的镜像。换句话说,你可以用它来定义你需要的构建环境。

当一个 Docker 镜像被构建时,整个 Docker 上下文 - 即你的项目的根在 COPY  ADD 命令执行之前就被发送给了 Docker 守护进程。

这可能是相当费资源,尤其是当你的项目中有许多依赖关系、大量的数据文件或构建工件时。

另外,当 Docker CLI 和守护程序不在同一台机器上。比如守护进程是在远程机器上执行的,你就更应该注意构建环境的大小了。

你应该在 .dockerignore 文件中添加什么?

  1. 临时文件和文件夹

  2. 构建日志

  3. 本地 secrets

  4. 本地开发文件,如 docker-compose.yml

  5. 版本控制文件夹,如 ".git"、".hg" 和 ".vscode" 等

例子:

**/.git
**/.gitignore
**/.vscode
**/coverage
**/.env
**/.aws
**/.ssh
Dockerfile
README.md
docker-compose.yml
**/.DS_Store
**/venv
**/env

总之,结构合理的 .dockerignore 可以帮助

  1. 减少 Docker 镜像的大小

  2. 加快构建过程

  3. 防止不必要的缓存失效

  4. 防止泄密

4. 检查并扫描你的 Dockerfile 和图像

Linting 是检查源代码中是否存在可能导致潜在缺陷的编程和风格错误以及不良做法的过程。就像编程语言一样,静态文件也可以被 lint。特别是对于你的 Dockerfile,linter 可以帮助确保它们的可维护性、避免弃用语法并遵守最佳实践。整理图像应该是 CI 管道的标准部分。

Hadolint[11] 是最流行的 Dockerfile linter:

hadolint Dockerfile

Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:7 DL3042 warning: Avoid the use of cache directory with pip. Use `pip install --no-cache-dir <package>`
Dockerfile:9 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile:17 DL3025 warningUse arguments JSON notation for CMD and ENTRYPOINT arguments

这是 Hadolint 一个在线的链接 https://hadolint.github.io/hadolint/ 也可以安装 VS Code 插件 [12]

你可以将 Dockerfile 与扫描图像和容器的漏洞结合使用。

以下是一些有影响力的镜像扫描工具:

  • Snyk[13] 是 Docker 本地漏洞扫描的独家提供商。你可以使用 docker scan CLI 命令来扫描图像。

  • Trivy[14] 可用于扫描容器镜像、文件系统、git 存储库和其他配置文件。

  • Clair[15] 是一个开源项目,用于对应用程序容器中的漏洞进行静态分析。

  • Anchore[16] 是一个开源项目,为容器镜像的检查、分析和认证提供集中式服务。

总而言之,对你的 Dockerfile 和图像进行 lint 和扫描,来发现任何偏离最佳实践的潜在问题。

5. 签名和验证图像

你怎么知道用于运行生产代码的图像没有被篡改?

篡改可以通过中间人(MITM)攻击或注册表被完全破坏来实现。Docker 内容信任(DCT)可以对来自远程注册中心的 Docker 镜像进行签名和验证。

为了验证镜像的完整性和真实性,请设置以下环境变量。

DOCKER_CONTENT_TRUST=1

现在,如果你试图拉一个没有被签名的镜像,你会收到以下错误。

Error: remote trust data does not exist for docker.io/namespace/unsigned-image:
notary.docker.io does not have trust data for docker.io/namespace/unsigned-image

你可以从使用 Docker 内容信任签署图像文档中了解签署图像的情况。

当从 Docker Hub 下 载图像时,确保使用官方图像或来自可信来源的经过验证的图像。较大的团队应该使用他们自己的内部私有容器仓库

6. 设置内存和 CPU 的限制

限制 Docker 容器的内存使用是一个好主意,特别是当你在一台机器上运行多个容器时。这可以防止任何一个容器使用所有可用的内存,从而削弱其他容器的功能。

限制内存使用的最简单方法是在 Docker cli 中使用 --memory  --cpu 选项。

docker run --cpus=2 -m 512m nginx

上述命令将容器的使用限制在 2 个 CPU 和 512 兆的内存。

你可以在 Docker Compose 文件中做同样的事情,像这样。

version: "3.9"
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: 2
          memory: 512M
        reservations:
          cpus: 1
          memory: 256M

请注意 reservations 字段。它是用来设置软限制的,当主机的内存或 CPU 资源不足时,它就会优先考虑。

其他相关资源

  1. 带有内存、CPU 和 GPU 的运行时选项:https://docs.docker.com/config/containers/resource_constraints/

  2. Docker Compose 的资源限制:https://docs.docker.com/compose/compose-file/compose-file-v3/#resources

总结

以上就是本文介绍的 17 条最佳实践,掌握这些最佳实践一定会让你的 Dockerfile 和 Docker Image 变得精简,干净,和安全。

AWS 云上安全最佳实践

AWS 云上安全最佳实践

[toc]

一、账号及访问管理

1.1、多 VPC 还是多账号模式

  • 单个团可以使用多 VPC 模式来管理和创建您的应用环境;
  • 多个团队可以使用多账户模式来管理和隔离您的应用环境。

1.2、多账户模式,选择主 master 账号

使用一个专门的主(root)账号,其上不启用任何 AWS 资源; 对这个主账户,启用 MFA。

可以使用 AWS Control Tower 服务。

二、系统架构安全

2.1、子网建议

  • 从每个可用区至少 2 个子网开始;
  • 使用子网来限制因特网的访问,如私有子网;
  • 考虑使用更大的子网(/24 或者更大)。

2.2、每个可用区子网划分

  • 从每个可用区至少 1 个公有子网,1 个私有子网开始;
  • 如果采用 3 层架构,建议使用 3 层子网结构,即公有子网、私有子网、敏感子网。

2.3、安全组的建议

  • 安全组默认允许所以出流量的规则;
  • 在安全组上修改这条缺省的出流量规则会增加复杂性,因此不推荐,除非有合规的要求;
  • 大多数企业为每类应用在安全组中配置入站规则;
  • 优先考虑使用安全组作为源;
  • 如果要安全组内实例通讯,请将源设为自己。

2.4、NACL 什么时候启用

  • 当需要禁止来自某特定源或者端口的流量时;
  • 当子网无需访问因特网时。

2.5、VPC 互联建议

  • 大多数的应用并不需要转移链路,或者数据传输带宽要求小于4Gbps,建议使用 ×××;
  • 如果应用需要更加稳定的链路,更大的带宽,更低的访问延时,考虑使用 Direct Connect。

2.6、IAM 建议

  • 将 IAM 策略应用到组,避免应用到单个用户;
  • 使用 IAM 角色可以避免在代码中硬编码用户访问秘钥;
  • 重要用户启用 MFA;
  • 务必配置密码策略,定期轮换秘钥和密码。

三、数据分级及保护

3.1、KMS 存储加密建议

根据企业需求或合规需求,对数据进行分级; 根据不同的数据级别制定不同的加密策略。

3.2、传输中的数据加密 TLS

  • 使用 ××× 对传输中的数据进行加密。
  • 使用 HTTPS 证书对传输中的数据进行加密,建议在 ELB 上卸载证书,ELB 至后端 EC2 明文传输;
  • 如果希望端到端加密,建议选择由 ELB 先卸载 TLS 证书,再同后端的 EC2 建立加密通道。

四、安全运维,监控及日志管理

4.1、CloudTrail 审计日志

  • 永远在所有的区域启用 CloudTrail;
  • 将 CloudTrail 日志存储到独立的审计账户 S3 存储桶中,并利用 S3 生命周期管理,长期保存。

4.2、VPC Flow Logs

  • 在 Trouble Shooting 时启用 Flow Logs;
  • 在应用测试,调试,试运行以及上线初期启用 Flow Logs;
  • 配合 Splunk 等商业软件使用。

4.3、AWS Config

建议启用 AWS Config。

4.4、Config Rule 启用建议

  • 从启用 AWS 托管规则开始;
  • 根据企业自身安全基线要求,或者合规要求,自己编写规则,实现 Compliance as Code。

五、实践相应及自动化

Docker 与 Kubernetes 最佳实践 - 架构师必备技能 docker 入门到专精高阶视频教程

Docker 与 Kubernetes 最佳实践 - 架构师必备技能 docker 入门到专精高阶视频教程

【直播预告】程序员逆袭 CEO 分几步?

Docker 与 Kubernetes 最佳实践 - 架构师必备技能 docker 入门到专精高阶视频教程

关于Docker 安全最佳实践和备忘单docker的安全性的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于10 个 Docker 镜像安全最佳实践、17 条 Docker 最佳实践、AWS 云上安全最佳实践、Docker 与 Kubernetes 最佳实践 - 架构师必备技能 docker 入门到专精高阶视频教程的相关知识,请在本站寻找。

本文标签: