云原生时代下的容器镜像安全(上)

大家好,我是张晋涛。
Kubernetes 作为云原生的基石,为我们带来了极大的便利性,越来越多的公司也都将 Kubernetes 应用到了生产环境中。然而,在享受其带来的便利性的同时,我们也需要关注其中的一些安全隐患。
本篇,我将为你重点介绍容器镜像安全相关的内容。
通常情况下,我们提到容器镜像安全,主要是指以下两个方面:

  • 镜像自身内容的安全;
  • 镜像分发过程的安全;
镜像自身内容的安全 要聊镜像自身内容的安全,那我们就需要知道镜像到底是什么,以及它其中的内容是什么。
镜像是什么
我们以 debian镜像为例,pull 最新的镜像,并将其保存为 tar 文件,之后进行解压:
?~ mkdir -p debian-image ?~ docker pull debian Using default tag: latest latest: Pulling from library/debian 647acf3d48c2: Pull complete Digest: sha256:e8c184b56a94db0947a9d51ec68f42ef5584442f20547fa3bd8cbd00203b2e7a Status: Downloaded newer image for debian:latest docker.io/library/debian:latest ?~ docker image save -o debian-image/debian.tar debian ?~ ls debian-image debian.tar ?~ tar -C debian-image -xf debian-image/debian.tar ?~ tree -I debian.tar debian-image debian-image ├── 827e5611389abf13dad1057e92f163b771febc0bcdb19fa2d634a7eb0641e0cc.json ├── b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda │├── json │├── layer.tar │└── VERSION ├── manifest.json └── repositories1 directory, 6 files

解压完成后,我们看到它是一堆 json 文件和 layer.tar文件的组合,我们再次对其中的 layer.tar进行解压:
?~ tar -C debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda -xf debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda/layer.tar ?~ tree -I 'layer.tar|json|VERSION'-L 1 debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda ├── bin ├── boot ├── dev ├── etc ├── home ├── lib ├── lib64 ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin ├── srv ├── sys ├── tmp ├── usr └── var19 directories, 0 files

解压后的目录结构想必你已经很熟悉了,是的,这是 rootfs的目录结构。
如果我们使用的是自己构建的一些应用镜像的话,经过几次解压,你也会在其中找到应用程序相对应的文件。
镜像自身内容安全如何保证
前面我们已经看到了容器镜像就是 rootfs和应用程序,以及一些配置文件的组合。所以要保证它自身内容的安全性,主要从以下几个方面来考虑:
rootfs安全 对应到我们的实际情况,rootfs通常是由我们使用的基础(系统)镜像提供的,或者也可以认为是我们构建镜像时,DockerfileFROM字段所配置的镜像提供的。
在这个方面想要做到安全性就需要我们:
  • 使用可信来源的镜像,比如 Docker 官方维护的镜像;
  • 对基础镜像持续的进行漏洞扫描和升级;
  • 也可以考虑使用 Distroless镜像,这样也可以一定程度上免受攻击;
应用程序 应用程序其实是我们自己提供的,在这方面想要做到安全性,那么就需要我们:
  • 持续的进行软件的漏洞扫描;
  • 对依赖及时的进行更新;
  • 可以考虑从 SDL(Security Development Lifecycle)过渡到 DevSecOps ;
配置文件 镜像中所包含的那些配置文件是由镜像构建工具所提供的,一般情况下,只要我们保证使用的镜像构建工具未被篡改或者留下什么漏洞,那么这里基本上不会有什么大的问题。
综合来看,我们可以直接使用类似 Trivy 或者 Anchore Engine 等镜像漏洞扫描工具来帮助我们保障镜像内容的安全。此外,一些镜像仓库,比如 Harbor 等都已经内置了镜像安全的扫描工具,或者可以使用 docker scan命令进行镜像的安全扫描。
镜像分发安全 镜像如何分发
我们首先来看看,容器镜像是怎么样从构建到部署到我们的 Kubernetes 环境中的。
云原生时代下的容器镜像安全(上)
文章图片

图 1 ,容器镜像自创建到发布部署的简要过程示意图
开发者在编写完代码后,推送代码到代码仓库。由此来触发 CI 进行构建,在此过程中会进行镜像的构建,以及将镜像推送至镜像仓库中。
在 CD 的环节中,则会使用镜像仓库中的镜像,部署至目标 Kubernetes 集群中。
那么在此过程中,攻击者如何进行攻击呢?
镜像分发中的安全问题
云原生时代下的容器镜像安全(上)
文章图片

图 2 ,镜像分发部署安全示例
如图,在镜像分发部署的环节中其上游是镜像仓库,下游是 Kubernetes 集群。对于镜像仓库而言,即使是内网的自建环境,由于我们的观念已从基于边界的安全转变为零信任安全,所以,我们统一以公共仓库为例来讲解。
攻击者可以通过一些手段进行劫持、替换成恶意的镜像,包括直接攻击镜像仓库等。
要保证部署到 Kubernetes 集群中镜像的安全性来源以及完整性,其实是需要在两个主要的环节上进行:
  • 构建镜像时进行镜像的签名;
  • 镜像分发部署时进行签名的校验;(下一篇内容继续)
我们来分别看一下。
镜像的标签和摘要
我们通常在使用容器镜像时有两种选择:
  • 标签,比如 alpine:3.14.3
  • 摘要,比如 alpine@sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2
大多数场景下,我们会直接使用标签,因为它的可读性更好。但是镜像内容可能会随着时间的推移而变化,因为我们可能会为不同内容的镜像使用相同的标签,最常见的就是 :latest标签,每次新版本发布的时候,新版本的镜像都会继续沿用 :latest标签,但其中的应用程序版本已经升级到了最新。
使用摘要的主要弊端是它的可读性不好,但是,每个镜像的摘要都是唯一的,摘要是镜像内容的 SHA256 的哈希值。所以我们可以通过摘要来保证镜像的唯一性。
通过以下示例可以直接看到标签和摘要信息:
?~ docker pull alpine:3.14.3 3.14.3: Pulling from library/alpine Digest: sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2 Status: Image is up to date for alpine:3.14.3 docker.io/library/alpine:3.14.3 ?~ docker image inspect alpine:3.14.3 | jq -r '.[] | {RepoTags: .RepoTags, RepoDigests: .RepoDigests}' { "RepoTags": [ "alpine:3.14.3" ], "RepoDigests": [ "alpine@sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2" ] }

那么如何来保证镜像的正确性/安全性呢?这就是镜像签名解决的主要问题了。
镜像签名解决方案
数字签名是一种众所周知的方法,用于维护在网络上传输的任何数据的完整性。对于容器镜像签名,我们有几种比较通用的方案。
Docker Content Trust (DCT) 在传输一般文件时,可能有过类似的经历,比如因为网络原因导致下载的文件不完整;或是遭遇中间人的攻击导致文件被篡改、替换等。
镜像在分发过程中其实也可能会遇到类似的问题,这就是此处我们要讨论的重点,也就是 Docker Content Trust(DCT)主要解决的问题。
Docker Content Trust 使用数字签名,并且允许客户端或运行时验证特定镜像标签的完整性和发布者。对于使用而言也就是 docker trust 命令所提供的相关功能。注意:这需要 Docker CE 17.12 及以上版本。
前面我们提到了,镜像记录可以有一些标签,格式如下:
[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

以标签为例,DCT 会与标签的一部分相关联。每个镜像仓库都有一组密钥,镜像发布者使用这些密钥对镜像标签进行签名。(镜像发布者可以自行决定要签署哪些标签)镜像仓库可以同时包含多个带有已签名标签和未签名标签的镜像。
这里需要说明下,如果镜像发布者先推送签名的 latest 镜像,再推送未签名的 latest 镜像,那么后一个镜像不会影响前一个镜像的内容(区别于上文中标签覆盖的地方)。
云原生时代下的容器镜像安全(上)
文章图片

图 4 ,DCT 镜像签名示例(图中简略了登录镜像仓库的认证过程)
在生产中,我们可以启用 DCT 确保使用的镜像都已签名。如果启用了 DCT,那么只能对受信任的镜像(已签名并可验证的镜像)进行拉取、运行或构建。
启用 DCT 有点像对镜像仓库应用“过滤器”,即,只能看到已签名的镜像标签,看不到未签名的镜像标签。如果客户端没有启用 DCT ,那么它可以看到所有的镜像。
这里我们来快速的看一下 DCT 的工作过程
它对镜像标签的信任是通过使用签名密钥来管理的。在我们首次开启 DCT 并使用的时候会创建密钥集。一个密钥集由以下几类密钥组成:
  • 一个离线密钥 offline key ,它是镜像标签 DCT 的根 (丢失根密钥很难恢复)
  • 对标签进行签名的存储库或标记密钥 tag key
  • 服务器管理的密钥,例如时间戳密钥
云原生时代下的容器镜像安全(上)
文章图片

图 5 , 镜像签名密钥示例
刚从我们提到客户端使用 DCT 也就是我们的 docker trust命令,它是建立在 Notary v1 上的。默认情况下,Docker 客户端中禁用 DCT 。要启用需要设置 DOCKER_CONTENT_TRUST=1 环境变量 。
效果如下:
?~ DOCKER_CONTENT_TRUST=1 docker pull alpine:3.12 Pull (1 of 1): alpine:3.12@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a docker.io/library/alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a: Pulling from library/alpine 188c0c94c7c5: Already exists Digest: sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a Status: Downloaded newer image for alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a Tagging alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a as alpine:3.12 docker.io/library/alpine:3.12

Notary v1 前面我们提到 DCT 是基于 Notary v1 实现的,不过这不是我们本篇的重点,所以这里仅对 Notary v1 做个简单的介绍。Notary 项目地址: https://github.com/notaryproj...
云原生时代下的容器镜像安全(上)
文章图片

图 6 ,Notary 客户端、服务器和签名相关的交互流程
过程1 - 身份认证,任何没有令牌的连接将被重定向到授权服务器(Docker Registry v2 身份认证);
过程2 - 客户端将通过 HTTPS 上的身份验证登录到授权服务器,获取令牌;
过程3 - 当客户端上传新的元数据文件时,服务器会根据以前的版本检查它们是否存在冲突,并验证上传的元数据的签名、校验和和有效性;
过程4 - 一旦所有上传的元数据都经过验证,服务器会生成时间戳(可能还有快照),然后将它们发送给 sign 进行签名;
过程5 - sign 从其数据库中检索加密私钥,解密密钥,并使用它们进行签名,并发送回服务器;
过程6 - 服务器将客户端上传和服务器生成的元数据存储在 TUF 库中。生成的时间戳和快照元数据证明客户端上传的元数据是该可信集合的最新版本。之后,服务器会通知客户端--上传成功;
过程7 - 客户端现在可以立即从服务器下载最新的元数据了。在时间戳过期的情况下,服务器将遍历整个序列,生成新的时间戳,请求 sign 签名,将新签名的时间戳存储在数据库中。然后,它将这个新的时间戳连同其他存储的元数据一起发送给请求客户端;
这个项目由于是一个安全项目,虽然用途很大,但整体并不活跃。现在正在进行 v2 版本的开发,有兴趣的小伙伴欢迎加入。
sigstore 和 Cosign 这里介绍另一个来自 Linux 基金会的项目,叫做 sigstore 它主要是为了提供一些标准的库/工具,便于更好的进行签名和校验。当然,目前 sigstore 已经汇聚了包括 Cosign,Fulcio 和 Rekor 等开源项目,涉及到镜像镜像签名校验和供应链等方面。
云原生时代下的容器镜像安全(上)
文章图片

图 7 ,sigstore 简介
Cosign 是 sigstore 的工具之一,用于 OCI registry 中创建、存储和验证容器镜像签名。Cosign v1.0 已于今年下半年发布,是否能稳定用于生产环境,还有待考验。 截止目前,Cosign 已经发布了 v1.3.1 版本,详细变更请参考其 ReleaseNote: https://github.com/sigstore/c...
【云原生时代下的容器镜像安全(上)】我们这里看下它如何进行镜像的签名
?cosign cosign generate-key-pair Enter password for private key: Enter password for private key again: Private key written to cosign.key Public key written to cosign.pub ?cosign cosign sign --key cosign.key ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb Enter password for private key: % ?cosign cosign verify --key cosign.pubghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb Verification for ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb -- The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key - Any certificates were verified against the Fulcio roots.[{"critical":{"identity":{"docker-reference":"ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo"},"image":{"docker-manifest-digest":"sha256:768845efa2a32bc5c5d83a6f7ec668b98f5db46585dd1918afc9695a9e653d2d"},"type":"cosign container image signature"},"optional":null}]

看起来还是比较简单的。
总结 以上就是关于镜像自身内容安全,以及镜像分发安全中的镜像签名校验部分的内容。
下一篇我将为大家介绍如何在镜像分发及部署时进行签名的校验,以及如何保护 Kubernetes 集群免受未签名或不可信来源镜像的攻击,敬请期待!
欢迎订阅我的文章公众号【MoeLove】
云原生时代下的容器镜像安全(上)
文章图片

    推荐阅读