前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在CRI运行中验证容器镜像签名

在CRI运行中验证容器镜像签名

作者头像
灵雀云
发布2023-08-09 11:26:14
3150
发布2023-08-09 11:26:14
举报

作者:Sascha Grunert

Kubernetes社区自v1.24版本开始对其基于容器镜像的工件进行签名。随着v1.26版本中相应增强功能从alpha版本升级为beta版本,引入了对二进制工件的签名。其他项目也采用了这种方法,为其发布提供了镜像签名。这意味着它们可以在自己的CI/CD流水线中创建签名,例如使用GitHub Actions,或者依靠Kubernetes镜像推广流程通过向k/k8s.io存储库提交拉取请求来自动签名镜像。使用此流程的要求是项目必须是kubernetes或kubernetes-sigs GitHub组织的一部分,以便利用社区基础设施将镜像推送到暂存存储桶中。

假设项目现在生成了已签名的容器镜像工件,那么如何验证这些签名呢?可以按照官方Kubernetes文档中概述的手动方式进行验证。这种方法的问题在于完全没有自动化,应该仅用于测试目的。在生产环境中,可以使用像sigstore policy-controller这样的工具来实现自动化。这些工具通过使用自定义资源定义(CRD)以及集成的准入控制器和webhook来提供更高级别的API来验证签名。

基于准入控制器的验证的一般使用流程如下:

这种架构的一个关键优势是简单性:集群中的单个实例在容器运行时节点上的任何镜像拉取之前验证签名,而镜像拉取是由kubelet发起的。然而,这种优势也带来了分离的问题:应该拉取容器镜像的节点不一定是执行准入的节点。这意味着如果控制器受到攻击,就无法实现集群范围的策略执行。

解决这个问题的一种方法是在符合容器运行时接口(CRI)的容器运行时中直接进行策略评估。运行时直接连接到节点上的kubelet,并执行拉取镜像等任务。CRI-O是其中一个可用的运行时,将在v1.28版本中提供完整的容器镜像签名验证支持。

它是如何工作的?CRI-O读取一个名为policy.json的文件,其中包含为容器镜像定义的所有规则。例如,您可以定义一个只允许任何标签或摘要为quay.io/crio/signed的已签名镜像的策略,如下所示:

代码语言:javascript
复制
{
  "default": [{ "type": "reject" }],
  "transports": {
    "docker": {
      "quay.io/crio/signed": [
        {
          "type": "sigstoreSigned",
          "signedIdentity": { "type": "matchRepository" },
          "fulcio": {
            "oidcIssuer": "https://github.com/login/oauth",
            "subjectEmail": "sgrunert@redhat.com",
            "caData": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUI5ekNDQVh5Z0F3SUJBZ0lVQUxaTkFQRmR4SFB3amVEbG9Ed3lZQ2hBTy80d0NnWUlLb1pJemowRUF3TXcKS2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweQpNVEV3TURjeE16VTJOVGxhRncwek1URXdNRFV4TXpVMk5UaGFNQ294RlRBVEJnTlZCQW9UREhOcFozTjBiM0psCkxtUmxkakVSTUE4R0ExVUVBeE1JYzJsbmMzUnZjbVV3ZGpBUUJnY3Foa2pPUFFJQkJnVXJnUVFBSWdOaUFBVDcKWGVGVDRyYjNQUUd3UzRJYWp0TGszL09sbnBnYW5nYUJjbFlwc1lCcjVpKzR5bkIwN2NlYjNMUDBPSU9aZHhleApYNjljNWlWdXlKUlErSHowNXlpK1VGM3VCV0FsSHBpUzVzaDArSDJHSEU3U1hyazFFQzVtMVRyMTlMOWdnOTJqCll6QmhNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCUlkKd0I1ZmtVV2xacWw2ekpDaGt5TFFLc1hGK2pBZkJnTlZIU01FR0RBV2dCUll3QjVma1VXbFpxbDZ6SkNoa3lMUQpLc1hGK2pBS0JnZ3Foa2pPUFFRREF3TnBBREJtQWpFQWoxbkhlWFpwKzEzTldCTmErRURzRFA4RzFXV2cxdENNCldQL1dIUHFwYVZvMGpoc3dlTkZaZ1NzMGVFN3dZSTRxQWpFQTJXQjlvdDk4c0lrb0YzdlpZZGQzL1Z0V0I1YjkKVE5NZWE3SXgvc3RKNVRmY0xMZUFCTEU0Qk5KT3NRNHZuQkhKCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0="
          },
          "rekorPublicKeyData": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFMkcyWSsydGFiZFRWNUJjR2lCSXgwYTlmQUZ3cgprQmJtTFNHdGtzNEwzcVg2eVlZMHp1ZkJuaEM4VXIvaXk1NUdoV1AvOUEvYlkyTGhDMzBNOStSWXR3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
        }
      ]
    }
  }
}

要使用该策略作为全局的真实源,需要启动CRI-O:

代码语言:javascript
复制
> sudo crio --log-level debug --signature-policy ./policy.json

现在,CRI-O可以在验证镜像签名的同时拉取镜像。可以使用crictl(cri-tools)来执行此操作,例如:

代码语言:javascript
复制
> sudo crictl -D pull quay.io/crio/signed
DEBU[…] get image connection
DEBU[…] PullImageRequest: &PullImageRequest{Image:&ImageSpec{Image:quay.io/crio/signed,Annotations:map[string]string{},},Auth:nil,SandboxConfig:nil,}
DEBU[…] PullImageResponse: &PullImageResponse{ImageRef:quay.io/crio/signed@sha256:18b42e8ea347780f35d979a829affa178593a8e31d90644466396e1187a07f3a,}
Image is up to date for quay.io/crio/signed@sha256:18b42e8ea347780f35d979a829affa178593a8e31d90644466396e1187a07f3a

CRI-O的调试日志还将指示签名已成功验证:

代码语言:javascript
复制
DEBU[…] IsRunningImageAllowed for image docker:quay.io/crio/signed:latest
DEBU[…]  Using transport "docker" specific policy section quay.io/crio/signed
DEBU[…] Reading /var/lib/containers/sigstore/crio/signed@sha256=18b42e8ea347780f35d979a829affa178593a8e31d90644466396e1187a07f3a/signature-1
DEBU[…] Looking for sigstore attachments in quay.io/crio/signed:sha256-18b42e8ea347780f35d979a829affa178593a8e31d90644466396e1187a07f3a.sig
DEBU[…] GET https://quay.io/v2/crio/signed/manifests/sha256-18b42e8ea347780f35d979a829affa178593a8e31d90644466396e1187a07f3a.sig
DEBU[…] Content-Type from manifest GET is "application/vnd.oci.image.manifest.v1+json"
DEBU[…] Found a sigstore attachment manifest with 1 layers
DEBU[…] Fetching sigstore attachment 1/1: sha256:8276724a208087e73ae5d9d6e8f872f67808c08b0acdfdc73019278807197c45
DEBU[…] Downloading /v2/crio/signed/blobs/sha256:8276724a208087e73ae5d9d6e8f872f67808c08b0acdfdc73019278807197c45
DEBU[…] GET https://quay.io/v2/crio/signed/blobs/sha256:8276724a208087e73ae5d9d6e8f872f67808c08b0acdfdc73019278807197c45
DEBU[…]  Requirement 0: allowed
DEBU[…] Overall: allowed

所有在策略中定义的字段,如oidcIssuer和subjectEmail,都必须匹配,而fulcio.caData和rekorPublicKeyData是上游fulcio(OIDC PKI)和rekor(透明日志)实例的公钥。

这意味着,如果您现在使策略中的subjectEmail无效,例如更改为wrong@mail.com:

代码语言:javascript
复制
> jq '.transports.docker."quay.io/crio/signed"[0].fulcio.subjectEmail = "wrong@mail.com"' policy.json > new-policy.json
> mv new-policy.json policy.json

然后删除该镜像,因为它已经存在于本地:

代码语言:javascript
复制
> sudo crictl rmi quay.io/crio/signed

现在当您拉取镜像时,CRI-O会报错,指出所需的电子邮件是错误的:

代码语言:javascript
复制
> sudo crictl pull quay.io/crio/signed
FATA[…] pulling image: rpc error: code = Unknown desc = Source image rejected: Required email wrong@mail.com not found (got []string{"sgrunert@redhat.com"})

也可以对未签名的镜像进行测试以验证策略。为此,您需要将键 quay.io/crio/signed 修改为类似 quay.io/crio/unsigned 的内容:

代码语言:javascript
复制
> sed -i 's;quay.io/crio/signed;quay.io/crio/unsigned;' policy.json

如果您现在拉取容器镜像,CRI-O会报告找不到该镜像的签名:

代码语言:javascript
复制
> sudo crictl pull quay.io/crio/unsigned
FATA[…] pulling image: rpc error: code = Unknown desc = SignatureValidationFailed: Source image rejected: A signature was required, but no signature exists

值得一提的是,CRI-O会匹配签名中的.critical.identity.docker-reference字段与镜像仓库进行匹配。例如,如果您验证的镜像是registry.k8s.io/kube-apiserver-amd64:v1.28.0-alpha.3,那么相应的docker-reference应该是registry.k8s.io/kube-apiserver-amd64:。

代码语言:javascript
复制
> cosign verify registry.k8s.io/kube-apiserver-amd64:v1.28.0-alpha.3 \
    --certificate-identity krel-trust@k8s-releng-prod.iam.gserviceaccount.com \
    --certificate-oidc-issuer https://accounts.google.com \
    | jq -r '.[0].critical.identity."docker-reference"'
…

registry.k8s.io/kubernetes/kube-apiserver-amd64

Kubernetes社区引入了registry.k8s.io作为各种镜像仓库的代理镜像。在kpromo v4.0.2发布之前,镜像使用的是实际镜像而不是registry.k8s.io进行签名。

代码语言:javascript
复制
> cosign verify registry.k8s.io/kube-apiserver-amd64:v1.28.0-alpha.2 \
    --certificate-identity krel-trust@k8s-releng-prod.iam.gserviceaccount.com \
    --certificate-oidc-issuer https://accounts.google.com \
    | jq -r '.[0].critical.identity."docker-reference"'
…

asia-northeast2-docker.pkg.dev/k8s-artifacts-prod/images/kubernetes/kube-apiserver-amd64

将docker-reference更改为registry.k8s.io使终端用户更容易验证签名,因为他们无法了解底层使用的基础设施。通过标志符号“--sign-container-identity”,签名功能已添加到cosign中,并将成为即将发布的版本的一部分。

最近,在Kubernetes中添加了用于镜像拉取错误的错误代码SignatureValidationFailed,并将从v1.28开始提供。此错误代码允许终端用户直接从kubectl CLI了解镜像拉取失败的原因。例如,如果您使用要求对quay.io/crio/unsigned进行签名的策略运行CRI-O和Kubernetes,则Pod定义如下:

代码语言:javascript
复制
apiVersion: v1
kind: Pod
metadata:
  name: pod
spec:
  containers:
    - name: container
      image: quay.io/crio/unsigned

应用该Pod清单将导致SignatureValidationFailed错误:

代码语言:javascript
复制
> kubectl apply -f pod.yaml
pod/pod created
代码语言:javascript
复制
> kubectl get pods
NAME   READY   STATUS                      RESTARTS   AGE
pod    0/1     SignatureValidationFailed   0          4s
代码语言:javascript
复制
> kubectl describe pod pod | tail -n8
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  58s                default-scheduler  Successfully assigned default/pod to 127.0.0.1
  Normal   BackOff    22s (x2 over 55s)  kubelet            Back-off pulling image "quay.io/crio/unsigned"
  Warning  Failed     22s (x2 over 55s)  kubelet            Error: ImagePullBackOff
  Normal   Pulling    9s (x3 over 58s)   kubelet            Pulling image "quay.io/crio/unsigned"
  Warning  Failed     6s (x3 over 55s)   kubelet            Failed to pull image "quay.io/crio/unsigned": SignatureValidationFailed: Source image rejected: A signature was required, but no signature exists
  Warning  Failed     6s (x3 over 55s)   kubelet            Error: SignatureValidationFailed

这种整体行为提供了更符合Kubernetes本机体验的方式,并且不依赖于在集群中安装第三方软件。

仍然有一些特殊情况需要考虑:例如,如果您想以与policy-controller支持的方式允许每个命名空间的策略,该怎么办?好吧,v1.28中即将推出的CRI-O功能就是为此而设计的!CRI-O将支持--signature-policy-dir / signature_policy_dir选项,该选项定义了用于命名空间分隔的签名策略的根路径。这意味着CRI-O将查找该路径并组装一个策略,例如<SIGNATURE_POLICY_DIR>/<NAMESPACE>.json,如果存在则在图像提取时使用。如果未通过图像提取提供Pod命名空间(通过sandbox配置),或者连接的路径不存在,则CRI-O将使用全局策略作为后备。

另一个要考虑的特殊情况对于容器运行时的正确签名验证至关重要:kubelet仅在磁盘上不存在图像时才调用容器图像提取。这意味着,来自Kubernetes命名空间A的无限制策略可以允许提取图像,而命名空间B无法强制执行该策略,因为图像已存在于节点上。最后,CRI-O不仅需要在图像提取时验证策略,还需要在容器创建时验证策略。这实际上使事情变得更加复杂,因为CRI在容器创建时不会传递用户指定的图像引用,而是已解析的图像ID或摘要。对CRI进行小的更改可以解决这个问题。

现在,所有操作都在容器运行时中进行,需要有人来维护和定义策略,以提供良好的用户体验。policy-controller的CRD非常好用,同时我们可以想象在集群中有一个守护程序为CRI-O每个命名空间编写策略。这将使任何额外的挂钩都变得不必要,并将验证图像签名的责任移交给实际提取图像的实例。我评估了在纯Kubernetes中实现更好的容器图像签名验证的其他可能途径,但是没有找到一个适合原生API的解决方案。这意味着我认为CRD是正确的方法,但用户仍然需要一个实际提供它的实例。

本文翻译自kubernetes.io

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-06-30,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 云原生技术社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com