如何优化Docker图像的安全性、大小和构建速度

2022-02-21 03:09:19

本文介绍了12个优化Docker图像安全性的技巧。对于每个技巧,它解释了底层的攻击向量,以及一个或多个缓解方法。提示包括避免泄露构建机密、以非root用户身份运行,或者如何确保使用最新的依赖项和更新。

本文是关于以优化方式使用Docker的多部分系列文章的一部分:

当您刚接触Docker时,很可能会创建不安全的Docker映像,使攻击者很容易接管容器,甚至整个主机,从而允许攻击者渗透到公司的其他基础设施。

有许多不同的攻击载体可以被滥用来接管您的系统,例如:

已启动的应用程序(在Dockerfile的入口点中指定)以root用户身份运行。因此,一旦攻击者利用漏洞并获得shell访问权限,他们就可以接管运行Docker守护程序的主机。

您的图像基于过时和/或不安全的基础图像,其中包含(现在)众所周知的安全漏洞。

您的图像中包含一些工具(如curl、apt等),一旦攻击者获得某种访问权限,攻击者就可以将更多恶意软件加载到容器中。

以下各节介绍了优化图像安全性的不同方法。它们按重要性/影响进行排序,以便首先列出更重要的。

构建机密是仅在构建Docker映像时(而不是在运行时)才需要的凭据。例如,您可能希望在映像中包含某个应用程序的编译版本,该应用程序的源代码是封闭源代码,其Git repo受访问保护。在构建映像时,需要克隆Git repo(这需要构建机密,例如该repo的SSH访问密钥),从源代码构建应用程序,然后再次删除源代码(和机密)。

“泄露”构建秘密意味着你不小心将这些秘密烘焙到了图像的某个层中。这很糟糕,因为任何拉取您的图像的人都可以检索凭据。这个问题源于这样一个事实:Docker图像是以一种纯粹的加法方式逐层构建的。你在图层中删除的文件只会被标记为已删除,但通过使用高级工具,每个人都可以访问你的图像。

Docker多阶段构建(官方文档)有许多不同的用例,例如加快图像构建或减小图像大小。本系列的其他文章详细介绍了这些其他用例。无论如何,您也可以通过多阶段构建来避免泄露构建机密,如下所示:

创建一个stage#a,将凭证复制到其中,并使用它们检索其他工件(例如上面示例中的Git repo),然后使用它们执行进一步的步骤(例如编译应用程序)。舞台#一个构建确实包含构建秘密!

创建一个stage#B,在其中只复制stage#a中的非机密人工制品,例如编译后的应用程序。

如果使用docker build进行构建,那么实际上会有多个后端执行构建。更新更快的后端是BuildKit,需要在Linux上通过设置环境变量DOCKER_BuildKit=1显式启用它。请注意,默认情况下,Windows/macOS上的Docker for Desktop会启用BuildKit。

正如这里的文档中所解释的(阅读它们以了解更多细节),BuildKit构建引擎支持Dockerfile中的其他语法。要使用构建机密,请在Dockerfile中输入以下内容:

这使得在执行RUN语句时生成容器可以使用机密,但不会将机密本身(此处为/foobar文件夹)放入生成的映像中。运行docker build命令时,需要指定机密文件/文件夹(位于主机上)的路径,例如docker build--secret id=mysecret,src=mysecret。txt-t sometag。

但是,有一个警告:您不能通过docker compose up--build构建需要机密的图像,因为docker compose还不支持构建的--secret参数,请参阅GitHub问题。如果您依赖docker compose构建来工作,请使用方法1(多阶段构建)。

您应该始终在干净的环境中生成和推送映像,例如CI/CD管道,在该管道中,生成代理会将您的存储库克隆到一个新目录中。

使用本地开发机器进行构建的问题是,Git存储库的本地“工作树”可能是脏的。例如,它可能包含开发过程中需要的机密文件,例如登台服务器甚至生产服务器的访问密钥。如果这些文件未通过排除。dockerignore,一种声明,如“复制…”在Dockerfile中,可能会意外地导致这些秘密泄漏到最终图像中。

默认情况下,当有人通过“docker run<;more arguments>;yourImage:yourTag”运行映像时,容器(以及您在ENTRYPOINT/CMD中的程序)将以root用户身份运行(在容器中和主机上)。这为攻击者提供了以下能力,攻击者通过某种攻击获得了对正在运行的容器的shell访问权限:

对主机上显式装入容器的所有目录的无限制写访问(由于是根目录)。

能够在容器中完成Linux root用户可以完成的所有操作。例如,攻击者可以安装加载更多恶意软件所需的其他工具,例如通过apt get install(非root用户不能这样做)。

如果图像的容器是以docker run--privileged启动的,攻击者甚至可以接管整个主机。

为了避免这种情况,您应该以非root用户的身份运行应用程序,即在docker构建过程中创建的某个用户。将以下语句放在Dockerfile中的某个位置(通常放在末尾):

#创建新用户(包括主目录,这是可选的)运行useradd--Create home appuser#切换到此用户appuser

Dockerfile中在用户appuser语句之后的所有命令(例如RUN、CMD或ENTRYPOINT)都将与此用户一起运行。有几点需要注意:

在切换到非root用户之前,通过COPY(或某些运行命令创建的文件)复制到映像中的文件归root所有,因此无法由以非root用户身份运行的应用程序写入。要解决此问题,请将创建并切换到非root用户的代码移到Dockerfile开头附近。

如果这些文件是在Dockerfile的开头以root user(存储在/root/下面,而不是/home/appuser/下面)的身份创建的,那么从应用程序的角度来看,程序希望位于用户主目录(例如~/.cache)某处的文件现在可能突然丢失。

如果应用程序侦听TCP/UDP端口,则应用程序必须使用端口>;1024.端口<;=1024只能作为根用户使用,或者具有较高的Linux功能,您不应该为了这个目的而将这些功能提供给容器。

如果您使用的基本映像包含真实Linux发行版(如Debian、Ubuntu或alpine映像)的整个工具集,包括软件包管理器,建议使用该软件包管理器安装所有可用的软件包更新。

基本映像由配置计划CI/CD管道的人员维护,这些管道构建基本映像并定期将其推送到Docker Hub。您无法控制这个时间间隔,而且通常情况下,Linux发行版的软件包注册表(例如,通过apt)中有安全补丁,然后管道将更新的Docker映像推送到Docker Hub。例如,即使每周推送一次基本映像,也可能在最近的映像发布几小时或几天后才提供安全更新。

因此,最好总是在无人值守模式下运行包管理器命令,更新本地包数据库并安装更新,而无需用户确认。每个Linux发行版的命令都不同。

例如,对于Ubuntu、Debian或衍生发行版,使用RUN-apt-get-update&&;apt-get-y升级

另一个重要的细节是,您需要告诉Docker(或您使用的任何图像构建工具)刷新基础图像。否则,如果引用python:3之类的基本映像(Docker在其本地映像缓存中已经有这样的映像),Docker甚至不会检查Docker Hub上是否存在python:3的更新版本。要消除此行为,应使用以下命令:

这可以确保Docker在构建您的映像之前,会从Dockerfile的FROM语句中获取映像的更新。

您还应该了解Docker的层缓存机制,它会导致图像过时,因为运行的层<;安装apt/etc更新>;命令被缓存,直到基本映像维护程序发布新版本的基本映像。如果发现基本映像的发布频率很低(例如,不到一周),最好定期(例如,每周一次)使用禁用的层缓存重建映像。可以通过运行以下命令来执行此操作:

您编写的软件基于第三方依赖关系,即由其他人制作的软件。这包括:

作为应用程序一部分使用的第三方软件组件,例如通过pip/npm/gradle/apt/安装的…

一旦这些依赖项在图像中过时,就会再次增加攻击面,因为过时的依赖项通常具有可利用的安全漏洞。

您可以通过定期使用SCA工具(软件组合分析)来解决这个问题,例如RefresentBot。这些工具(半)自动将您声明的第三方依赖项更新为其最新版本,例如在Dockerfile中,Python的需求。txt,NPM的软件包。json等。您需要设计CI管道,以便SCA工具所做的更改自动触发映像的重新构建。

这种自动触发的图像重建对于仅处于维护模式的项目尤其有用,但这些项目中的代码仍应由客户(他们希望代码是安全的)在生产中使用。在维护期间,您不再开发新功能,也不会生成新映像,因为没有新提交(由您进行)触发新生成。但是,SCA工具所做的提交确实会再次触发映像构建。

即使您实现了上述建议,使您的图像始终使用最新的第三方依赖项,它仍然可能是不安全的(例如,如果依赖项已被放弃)。在这种情况下,“不安全”意味着一个(或多个)依赖项具有已知的安全漏洞(在某些CVE数据库中注册)。

出于这个原因,您可以使用各种工具提供Docker映像,它们会扫描所有包含的文件以查找此类漏洞。这些工具有两种形式:

您明确调用的CLI工具,例如在CI管道中,例如Trivy(在CI管道中非常容易使用的OSS,请参阅Trivy docs)、Clair(OSS,但设置和使用起来比Trivy更复杂)或Snyk(通过“Docker scan”集成到Docker CLI中,请参阅备忘单,但只有有限的免费计划!)

扫描仪集成在图像注册表中,您可以将图像推入其中,例如Harbor(内部使用Clair或Trivy)。还有一些商业产品,如Anchore。

因为这些扫描器是通用的,并且试图覆盖范围广泛的包注册中心,所以它们可能不会专门用于您在项目中使用的编程语言或包注册中心。有时,调查您的编程语言生态系统提供了哪些工具是有意义的。例如,对于Python,有一个专门针对Python包的安全工具。

有时,问题源于你在Dockerfile中的陈述,这是一种糟糕的做法(你没有意识到)。使用checkov、Conftest、trivy或hadolint等工具,它们是Dockerfiles的过梁。要为该工具做出正确的选择,请查看它附带了哪些默认规则/策略。例如,hadolint提供的规则比checkov或conftest多得多,因为它专门用于DockerFile。这些工具也相互补充,因此在Dockerfiles上运行多个工具(例如hadolint和trivy)是有意义的。不过,要做好准备,在某些规则被忽略的情况下,您需要维护“忽略文件”,例如,由于误报,或者因为您故意违反规则。

要验证您使用的基础图像是否真的构建了&;在这张图片背后的公司推动下,你可以使用Docker Content Trust(见官方文档)。只需将DOCKER_CONTENT_TRUST环境变量设置为";1" 在运行docker build时。Docker守护进程将拒绝提取未经发布者签名的图像。

安全问题通常源于其他人的代码问题,也就是说,流行的第三方依赖关系,因为它们分布广泛,所以黑客“有利可图”。然而,有时是你自己的代码造成的。例如,您可能意外地实现了SQL注入可能性、堆栈溢出漏洞等。

为了发现这些问题,有所谓的SAST工具(静态应用程序安全测试)。一方面,有一些特定于编程语言的工具(你必须单独研究),比如Python的bandit,或者Java的Checkstyle/Spotbugs。另一方面,还有支持多种编程语言和框架的工具套件(其中一些是非免费/商业的),比如SonarQube(SonarLint IDE插件也支持SonarQube)。请参见此处以获取SAST工具的列表。

连续(自动)扫描:创建一个CI作业,每次推送时扫描代码。这会不断地将代码的安全性保持在一个较高的水平上,但您必须弄清楚如何忽略误报(这是一项持续的维护工作)。如果您使用GitLab,您可能还会发现GitLab的免费SAST功能很有趣。

偶尔(手动)扫描:团队中一些安全意识强的成员在本地运行安全检查,例如每月一次或每次发布前,并手动查看结果。

docker slim工具获取大型docker图像,临时运行它们,分析临时容器中真正使用的文件,然后生成一个新的单层docker图像,其中所有未使用的文件都已删除。这有两个好处:

图像变得更加安全,因为删除了不需要的工具(例如curl或package manager)

图像中存储的软件(如CLI工具等)越多,攻击面就越大。使用“最小”图像是一种很好的做法,它的尺寸尽可能小(无论如何这是一个很好的优势),并且包含尽可能少的工具。最小图像甚至超出了“大小优化”图像(例如alpine或<;某物>;:<;版本>;-slim,例如python:3.8-slim)的功能:它们没有任何包管理器。这使得攻击者很难加载其他工具。

最安全的最小基本映像是SCRATCH,它完全不包含任何内容。只有在映像中放置包含所有依赖项(包括C运行时)的自包含二进制文件时,才可以从头开始Dockerfile。

如果SCRATCH不适用于您,那么Google的Distorless映像可能是一个不错的选择,尤其是当您正在为Python或Node等常见编程语言构建应用程序时。js,或者需要Debian的最小基本映像。

发布警告:使用谷歌在gcr上发布的特定于编程语言的图片。不建议使用io,因为只有最新版本的标签,以及主要版本的标签(例如,python为“3”,节点为“12”)。您无法控制特定语言的运行时版本(例如,是否使用Python 3.8.3或3.8.4等),这会破坏图像构建的再现性。

定制(并构建自己的)无发行版本映像非常复杂:您需要熟悉Bazel构建系统并自己构建映像注意:如果您需要的唯一定制是以非root用户的身份运行代码,那么默认情况下,每个无发行版本基映像中都有一个非root用户,请参见此处了解详细信息。

一般来说,最小基本映像的警告:使用最小基本映像调试容器是很棘手的,因为Docker现在缺少有用的工具(例如/bin/sh),所以可以运行第二个调试容器(它确实有一个shell和调试工具,例如alpine:latest),并使其共享最小容器的PID命名空间,例如,通过docker run-it-rm-pid=container:<;最小容器id>--加帽系统

受信任映像是指已由某人(您自己的组织或其他人)审核的映像,例如具有安全级别的映像。这对于具有高安全要求和法规的受监管行业(银行业、航空航天等)尤其重要。

虽然审核可以由您自己完成,但不鼓励您自己从头开始构建受信任的映像,因为作为映像生成器,您必须确保完成所有与审核相关的任务并正确记录(例如,记录映像中的包列表、执行的CVE检查及其结果等)。这是很多工作。相反,建议使用商业“可信注册中心”将此任务外包,该注册中心提供一组选定的可信映像,例如RedHat的Universal Base images(UBI)。RedHat的UBI现在也可以在Docker Hub上免费获得。

Docker Hub上托管的图像未经审核。它们“按原样”提供。它们可能不安全(甚至包含恶意软件),没有人会通知你。因此,使用Docker Hub images中不安全的基础图像也会使您的图像不安全。

此外,你不应该把审计和Docker的内容信任混为一谈,如上所述!内容信任仅确认源(图像上传者)的身份,不说明任何有关图像安全的事实。

Linux功能是一种Linux内核功能,允许您控制应用程序可能使用的内核功能。例如,进程是否可以发送信号(例如SIGKILL)、配置网络接口、装载磁盘或调试进程。完整列表请参见此处。一般来说,应用程序需要的功能越少越好。

任何启动图像容器的人都可以提供(或带走)这些功能,例如,通过调用“docker run--cap drop=ALL<;image>;”。默认情况下,Docker会删除除此处定义的功能之外的所有功能。您的应用程序可能不需要所有这些。

作为最佳实践,尝试启动映像容器,删除所有功能(使用--cap drop=all),然后看看它是否仍然有效。如果没有,请找出缺少哪些功能,您是否真的需要它们,如果确实需要,请记录您的映像需要哪些功能(以及为什么需要),这会增加运行映像的人的信任。

让你的形象安全无需在公园里散步。评估和实施每项实践都需要时间。本文中的列表应该可以节省您的时间,因为它已经完成了收集必要步骤并确定其优先级的工作。

幸运的是,保护应用程序是一个迭代过程。你可以从小事做起,一步一个脚印地完成。不过,你确实需要得到管理层的认可。这有时很棘手,尤其是如果你的经理拒绝建议,并且他们倾向于

......