作者: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的已签名镜像的策略,如下所示:
{
"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:
> sudo crio --log-level debug --signature-policy ./policy.json
现在,CRI-O可以在验证镜像签名的同时拉取镜像。可以使用crictl(cri-tools)来执行此操作,例如:
> 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的调试日志还将指示签名已成功验证:
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:
> jq '.transports.docker."quay.io/crio/signed"[0].fulcio.subjectEmail = "wrong@mail.com"' policy.json > new-policy.json
> mv new-policy.json policy.json
然后删除该镜像,因为它已经存在于本地:
> sudo crictl rmi quay.io/crio/signed
现在当您拉取镜像时,CRI-O会报错,指出所需的电子邮件是错误的:
> 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 的内容:
> sed -i 's;quay.io/crio/signed;quay.io/crio/unsigned;' policy.json
如果您现在拉取容器镜像,CRI-O会报告找不到该镜像的签名:
> 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:。
> 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进行签名。
> 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定义如下:
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: container
image: quay.io/crio/unsigned
应用该Pod清单将导致SignatureValidationFailed错误:
> kubectl apply -f pod.yaml
pod/pod created
> kubectl get pods
NAME READY STATUS RESTARTS AGE
pod 0/1 SignatureValidationFailed 0 4s
> 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
转载请注明出处:https://www.cloudnative-tech.com/technology/5711.html