這篇文章已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

在 CRI 執行階段中驗證容器映像簽章

自 v1.24 版本以來,Kubernetes 社群一直在為其基於容器映像檔的人工產物簽署憑證。雖然 對應增強功能從 v1.26 中的 alpha 升級到 beta 後,引入了二進制人工產物的簽名,但其他專案也紛紛效仿,為其發布版本提供映像檔簽名。這表示他們要么在其自身的 CI/CD 管道中建立簽名,例如透過使用 GitHub 動作,要么依賴 Kubernetes 映像檔提升流程來自動簽署映像檔,方法是向 k/k8s.io 儲存庫提出提取請求。使用此流程的要求是專案必須是 kuberneteskubernetes-sigs GitHub 組織的一部分,以便他們可以利用社群基礎設施將映像檔推送到暫存儲存桶中。

假設一個專案現在產生了已簽署的容器映像檔人工產物,那麼實際上該如何驗證簽名呢?可以像 官方 Kubernetes 文件中概述的那樣手動執行。這種方法的問題在於它完全沒有自動化,應該僅用於測試目的。在生產環境中,像 sigstore policy-controller 這樣的工具可以幫助實現自動化。這些工具透過使用 自訂資源定義 (CRD) 以及集成的 准入控制器和 webhook 來驗證簽名,從而提供更高等級的 API。

基於准入控制器的驗證的一般使用流程是

Create an instance of the policy and annotate the namespace to validate the signatures. Then create the pod. The controller evaluates the policy and if it passes, then it does the image pull if necessary. If the policy evaluation fails, then it will not admit the pod.

此架構的一個主要優點是簡單性:叢集內的單個實例會在節點上的容器運行時中發生任何映像檔提取之前驗證簽名,這是由 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

策略中定義的所有欄位(例如 oidcIssuersubjectEmail)都必須匹配,而 fulcio.caDatarekorPublicKeyData 是來自上游 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://# \
    | 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://# \
    | 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 --sign-container-identity 新增到 cosign,並且將成為其即將發布版本的一部分。

Kubernetes 映像檔提取錯誤代碼 SignatureValidationFailed最近添加到 Kubernetes,並且將從 v1.28 開始提供。此錯誤代碼允許終端使用者直接從 kubectl CLI 了解映像檔提取失敗的原因。例如,如果您將 CRI-O 與 Kubernetes 一起使用,並使用需要簽署 quay.io/crio/unsigned 的策略,則如下所示的 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 支援的方式相同,允許每個命名空間的策略該怎麼辦?嗯,CRI-O v1.28 中即將推出一項功能來解決此問題!CRI-O 將支援 --signature-policy-dir / signature_policy_dir 選項,該選項定義了 Pod 命名空間分隔簽名策略的根路徑。這表示 CRI-O 將查找該路徑並組裝一個類似 <SIGNATURE_POLICY_DIR>/<NAMESPACE>.json 的策略,如果存在,則將在映像檔提取時使用。如果在映像檔提取時未提供 Pod 命名空間(透過 sandbox config),或者串聯的路徑不存在,則 CRI-O 的全局策略將用作後備。

另一個需要考慮的邊角案例對於容器運行時中正確的簽名驗證至關重要:僅當映像檔在磁碟上尚不存在時,kubelet 才會調用容器映像檔提取。這表示來自 Kubernetes 命名空間 A 的不受限制的策略可以允許提取映像檔,而命名空間 B 無法強制執行該策略,因為它已在節點上存在。最後,CRI-O 不僅需要在映像檔提取時驗證策略,還需要在容器建立時驗證策略。這個事實使事情變得更加複雜,因為 CRI 並沒有真正傳遞使用者在容器建立時指定的映像檔參考,而是已經解析的映像檔 ID 或摘要。對 CRI 的小改動可以解決這個問題。

既然一切都在容器運行時中發生,那麼就必須有人維護和定義策略,以圍繞該功能提供良好的使用者體驗。policy-controller 的 CRD 很棒,同時我們可以想像叢集內的守護進程可以為每個命名空間編寫 CRI-O 的策略。這將使任何額外的 hook 都變得過時,並將驗證映像檔簽名的責任轉移到實際提取映像檔的實例。 我評估了在純 Kubernetes 中實現更好容器映像檔簽名驗證的其他可能路徑,但我找不到非常適合原生 API 的方案。這表示我相信 CRD 是正確的方向,但使用者仍然需要一個實際提供它的實例。

感謝您閱讀這篇網誌文章!如果您對更多內容感興趣、提供回饋或尋求幫助,請隨時透過 Slack (#crio)SIG Node 郵件列表直接與我聯繫。