🚧

Ratify を使って kubernetes イメージの署名を検証する

に公開

Ratify とは

Ratify は kubernetes pod を作成する際にコンテナイメージの OCI アーティファクトを検証するための検証エンジンです。CNCF の Sandbox プロジェクト に採択されています。

https://ratify.dev/

k8s admission webhook + OPA Gatekeeper と組み合わせて使用し、pod 作成時にイメージが正式な Issuer によって署名されているか、脆弱性検出の結果をアーティファクトに含んでいるかなどを検証し、検証結果に応じて pod 作成を許可・拒否することができます。

コンテナイメージへの署名はイメージが第三者によって改竄されることを防いで信頼性を高めるために使用されます。ただ通常の設定ではコンテナ作成時に署名の検証などは行わないため署名のないイメージで pod を起動することも可能であり、これによって意図しないイメージが使用されるリスクは完全には排除しきれません。
Ratify では比較的簡単な手順により署名の検証を強制することができるため、信頼できるイメージのみを k8s クラスタにデプロイすることが可能になります。その他にイメージの脆弱性検出結果や SBOM などのアーティファクトとして保存された一連の成果物も検証することが可能です。
このような機能により、k8s のサプライチェーンにおけるセキュリティの担保やコンプライアンスの遵守といった観点で有効となっています。


Ratify を組み込んだイメージのビルド ~ デプロイの一連の流れ。ratify の検証ステップを組み込むことで有効な署名を含むイメージのみをクラスタにデプロイできる。https://ratify.dev/blog/sign-and-verify-image-with-notation-ratify より引用

クイックスタートを試す

まずは試すほうが早いので、以下のクイックスタートに従って動作を確認します。

インストール

Ratify は OPA Gatekeeper と合わせて使用するため、まずは helm で gatekeeper をインストールします。

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper/gatekeeper  \
    --name-template=gatekeeper \
    --namespace gatekeeper-system --create-namespace \
    --set enableExternalData=true \
    --set validatingWebhookTimeoutSeconds=5 \
    --set mutatingWebhookTimeoutSeconds=2 \
    --set externaldataProviderResponseCacheTTL=10s

次に ratify 本体を helm でインストールします。

helm repo add ratify https://ratify-project.github.io/ratify
# download the notary verification certificate
curl -sSLO https://raw.githubusercontent.com/deislabs/ratify/main/test/testdata/notation.crt
helm install ratify \
    ratify/ratify --atomic \
    --namespace gatekeeper-system \
    --set-file notationCerts={./notation.crt} \
    --set featureFlags.RATIFY_CERT_ROTATION=true \
    --set policy.useRego=true

インストールが完了すると gatekeeper-system に gatekeeper の controller manager x3, audit pod x1 と ratify pod x1 が作成されます。

$ k get pod
NAME                                             READY   STATUS    RESTARTS   AGE
gatekeeper-audit-bfd45555f-cccjv                 1/1     Running   0          24h
gatekeeper-controller-manager-795b55cc54-4dq65   1/1     Running   0          24h
gatekeeper-controller-manager-795b55cc54-7zdbf   1/1     Running   0          24h
gatekeeper-controller-manager-795b55cc54-fq4fh   1/1     Running   0          24h
ratify-869c8d9b5-l97gz                           1/1     Running   0          24h

サンプルイメージの検証

Ratify がイメージの署名を検証する動作を確認するため、ratify 側で用意されている署名済みのイメージを使って見ていきます。まずは以下の OPA constraint リソースを適用します。

kubectl apply -f https://ratify-project.github.io/ratify/library/default/template.yaml
kubectl apply -f https://ratify-project.github.io/ratify/library/default/samples/constraint.yaml
適用するテンプレートの内容

ここで適用するテンプレートは Ratify 固有のものではなく、Gatekeeper 側で検証を行うための Rego policy テンプレートになっています。

template.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: ratifyverification
spec:
  crd:
    spec:
      names:
        kind: RatifyVerification
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package ratifyverification

        # Get data from Ratify
        remote_data := response {
          images := [img | img = input.review.object.spec.containers[_].image]
          response := external_data({"provider": "ratify-provider", "keys": images})
        }

        # Base Gatekeeper violation
        violation[{"msg": msg}] {
          general_violation[{"result": msg}]
        }

        # Check if there are any system errors
        general_violation[{"result": result}] {
          err := remote_data.system_error
          err != ""
          result := sprintf("System error calling external data provider: %s", [err])
        }

        # Check if there are errors for any of the images
        general_violation[{"result": result}] {
          count(remote_data.errors) > 0
          result := sprintf("Error validating one or more images: %s", remote_data.errors)
        }

        # Check if the success criteria is true
        general_violation[{"result": result}] {
          subject_validation := remote_data.responses[_]
          subject_validation[1].isSuccess == false
          result := sprintf("Subject failed verification: %s", [subject_validation[0]])
        }

処理自体は gatekeeper の external data を使って ratify pod にイメージのデータを送信して検証を行い、返却された検証結果のフィールド (isSuccess) などを確認する内容となっています。

Ratify はあくまでイメージの署名を元に検証結果の json (後述) を gatekeeper 側に返すだけなので、結果の json を見て検証結果が ok/ng を判定したり、それに基づいて pod の作成許可・拒否を決めるのは gatekeeper 側の担当範囲になっています。

constraint.yaml の方は repo policy の検証が NG となった際に pod 作成を拒否するための constraint リソースになっています。

constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RatifyVerification
metadata:
  name: ratify-constraint
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["default"]

上記の通り制限が適用されるのは default namespace の pod だけなので、これ以外の namespace で pod を作った場合はイメージに署名がなくても起動します。

次に事前に署名済みのイメージ ghcr.io/deislabs/ratify/notary-image:signed を使った pod を起動します。

$ kubectl run demo --image=ghcr.io/deislabs/ratify/notary-image:signed -n default
pod/demo created

上記のイメージは notary によって署名されているため ratify の検証に合格し、特に問題なく起動に成功します。このとき ratify pod のログに検証の結果などが記録されます。トップレベルの "isSuccess": true から検証に成功していることが確認できます。

ratify pod ログ
time=2025-04-25T11:01:47.02736876Z level=info msg=verification response for subject ghcr.io/deislabs/ratify/notary-image@sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b:
{
  "version": "1.1.0",
  "isSuccess": true,
  "traceID": "fe09bc21-f019-453f-973a-9ceb8a174b5d",
  "timestamp": "2025-04-25T11:01:47.02734828Z",
  "verifierReports": [
    {
      "subject": "ghcr.io/deislabs/ratify/notary-image@sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b",
      "referenceDigest": "sha256:57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e",
      "artifactType": "application/vnd.cncf.notary.signature",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Notation signature verification success",
          "name": "verifier-notation",
          "verifierName": "verifier-notation",
          "type": "notation",
          "verifierType": "notation",
          "extensions": {
            "Issuer": "CN=Ratify Sample,O=Ratify",
            "SN": "CN=ratify.default"
          }
        }
      ],
      "nestedReports": []
    }
  ]
} component-type=server go.version=go1.23.4 namespace= trace-id=fe09bc21-f019-453f-973a-9ceb8a174b5d

次に署名されていないイメージ ghcr.io/deislabs/ratify/notary-image:unsigned の pod を起動してみます。

$ kubectl run demo1 --image=ghcr.io/deislabs/ratify/notary-image:unsigned -n default
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [ratify-constraint] Subject failed verification: ghcr.io/deislabs/ratify/notary-image@sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4

署名されていないイメージでは ratify の検証に通らないため、上記のようにエラーが表示され pod が作成されません。
このときの ratify pod のログは以下。unsigned タグのイメージは digest 17490f に解決されていますが、"isSuccess": false より検証に失敗していることが確認できます。

ratify pod ログ
time=2025-04-25T11:01:11.16119054Z level=info msg=mutating image ghcr.io/deislabs/ratify/notary-image:unsigned component-type=server go.version=go1.23.4 trace-id=15e07d95-6866-43b1-ab20-5c515e06355c
time=2025-04-25T11:01:11.940715693Z level=info msg=verifying subject ghcr.io/deislabs/ratify/notary-image@sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4 component-type=server go.version=go1.23.4 namespace= trace-id=be2995d9-7757-451d-85df-5cf510a42b29
time=2025-04-25T11:01:11.940773415Z level=info msg=Resolve of the image completed successfully the digest is sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4 component-type=executor go.version=go1.23.4 namespace= trace-id=be2995d9-7757-451d-85df-5cf510a42b29
time=2025-04-25T11:01:13.186862518Z level=info msg=verification response for subject ghcr.io/deislabs/ratify/notary-image@sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4:
{
  "version": "1.1.0",
  "isSuccess": false,
  "traceID": "be2995d9-7757-451d-85df-5cf510a42b29",
  "timestamp": "2025-04-25T11:01:13.18683787Z"
} component-type=server go.version=go1.23.4 namespace= trace-id=be2995d9-7757-451d-85df-5cf510a42b29
time=2025-04-25T11:01:44.740540047Z level=info msg=verifying subject ghcr.io/deislabs/ratify/notary-imag

署名の内容について深振りする

ratify がイメージの署名を見て pod 起動時に検証していることはわかりましたが、そもそもイメージはどのように署名されているのか、どのような署名が検証されているかは不明瞭な部分が多いためもう少し深く見ていきます。

イメージの署名情報を確認する方法はいくつかありますが、ここではイメージを含む OCI アーティファクトに署名したり、署名を検証できる CLI ツール notation を使います。

https://github.com/notaryproject/notation

notation ls [image] でイメージの署名を検証できるので、先程使用した署名済みイメージ notary-image:signed にどのような署名が設定されているか見てみます。

$ notation ls ghcr.io/deislabs/ratify/notary-image:signed
Warning: Always list the artifact using digest(@sha256:...) rather than a tag(:signed) because resolved digest may not point to the same signed artifact, as tags are mutable.
ghcr.io/deislabs/ratify/notary-image@sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
└── application/vnd.cncf.notary.signature
    └── sha256:57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e

この結果から以下のことがわかります。

  • notary-image:signed のイメージ部分はダイジェスト 8e3d01 に対応。
  • 8e3d01application/vnd.cncf.notary.signature によって署名されている。
  • 署名の詳細は 57be2c に対応。

一方で notary-image:unsigned のイメージでは署名がないことも確認できます。

$ notation ls ghcr.io/deislabs/ratify/notary-image:unsigned
Warning: Always list the artifact using digest(@sha256:...) rather than a tag(:unsigned) because resolved digest may not point to the same signed artifact, as tags are mutable.
ghcr.io/deislabs/ratify/notary-image@sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4 has no associated signature

では署名に関する情報が格納されている署名マニフェスト 57be2c の中身を見ていきます。
マニフェストの中身を取得するために、ここでは OCI アーティファクトを扱う CLI oras を利用します。oras の詳細は以前に書いた以下の記事を参照。

https://zenn.dev/zenogawa/articles/oras_oci_artifact

oras copy で署名マニフェスト 57be2c をダウンロードして output ディレクトリに展開します。

$ oras copy \
  ghcr.io/deislabs/ratify/notary-image@sha256:57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e \
  --to-oci-layout output

中身は以下。

output
├── blobs
│   └── sha256
│       ├── 2408cc74d12b6cd092bb8b516ba7d5e290f485d3eb9672efc00f0583730179e8
│       ├── 2558cbd97e0483e3bd0c66a3f03f471949c2a6c2fcb5d48a8ca577f22f1653fa
│       ├── 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
│       ├── 57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e
│       ├── 590382c032d581e30d154bb5c4338d36f417d1649e5d3ae459c90f889d97251c
│       ├── 74aaf3ca1c2baf0f0f878c8b4a470e208c46e04c29b36ad106e4523c5f648fc0
│       ├── 89b865d5710c99cddea45ece6dec6e01a7fea6fc62ce28f667fb7bf26b124b82
│       └── 8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
├── index.json
├── ingest
└── oci-layout

先程見たように 57be2c の blob (json) が署名に対応しているので中身を見てみます。

57be2c
schemaVersion: 2
mediaType: application/vnd.oci.image.manifest.v1+json
config:
  mediaType: application/vnd.cncf.notary.signature
  digest: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
  size: 2
layers:
  - mediaType: application/jose+json
    digest: sha256:74aaf3ca1c2baf0f0f878c8b4a470e208c46e04c29b36ad106e4523c5f648fc0
    size: 3100
subject:
  mediaType: application/vnd.docker.distribution.manifest.v2+json
  digest: sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
  size: 942
annotations:
  io.cncf.notary.x509chain.thumbprint#S256: '["af3390cf92e2077e02f6dcb531a8638241dfc0061851ca698f8c900ae79c1b68","3b1393351a3ccd30caca18ecd4096c0591cd9c18feac6ea8d4c6ddbf543734f5"]'
  org.opencontainers.image.created: "2023-06-29T07:01:15Z"

この中には config, layers, subject などが含まれていますが、これは notary における署名の仕様を記載した OCI signature の Signature Manifest に対応しています。それぞれのプロパティは以下に対応。

properties describe
config notary 署名プロジェクトでは mediaType: application/vnd.cncf.notary.signature が設定される。OCI イメージマニフェストとの互換性のために設定されるが使用されない
layers 署名に関する情報 (signature envelope) が含まれたマニフェストを指す
subject どのアーティファクトに対して署名されているかを表す。この場合はイメージ 8e3d01 に対して署名を行っている。
annotations メタデータなど

署名に関するデータの詳細は layer.digest に含まれているので、さらに 74aaf3 の中身を確認します。

74aaf3
payload: eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6OGUzZDAxMTEzMjg1YTBlNGFhNTc0ZGE4ZWI5YzBmMTEyYTFlYjk3OWQ3MmY3MzM5OWQ3MTc1YmEzY2RiMWMxYiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ
protected: eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIzLTA2LTI5VDA3OjAxOjE1WiJ9
header:
  x5c:
    - MIIC8DCCAdigAwIBAgIUNvsUlYEIROiCGAZ6881xIkDcYjQwDQYJKoZIhvcNAQELBQAwKTEPMA0GA1UECgwGUmF0aWZ5MRYwFAYDVQQDDA1SYXRpZnkgU2FtcGxlMB4XDTIzMDYyOTA1MjgzMloXDTMzMDYyNjA1MjgzMlowGTEXMBUGA1UEAwwOcmF0aWZ5LmRlZmF1bHQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD4iOEtnMcq4rv3ojkKbl1ErcOwiztYPAlUWT6BEyDBylG9sIPv5Qmj4IviaDsD+RTu+vf5ai7WDDLQ/BT4tM2QyKYtNPlc1BHgPkikgJ5hP/kzBabZ9no3ceg8pZdQog1bQrGCJvSnYckZjO1ivmfiroejJmwdpHuWObvHSGOxCtJHKM1Ml64lqfm6Yg2WOh/QVzCYYpS7W6yavdZ1DBI/sCozK2KZAYpnqtqSNSzY2j51K5quDCOERiYCmT2hNCDtk7CDpK61emLBi1290tB6iRx6XRv48bU6IxwanymVXRgDwqT8NZYjXVSgzSW5+kcVHLwrSbz+E/Az+I9B6PFNAgMBAAGjIDAeMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQARbDeFkjcGvZdH1nhoOmIMnAbBx/yFZR4f/AkrKLt4kRfjtma6lZim59kOxu/QV2oV65PLFJ9WVjQmMmEjGIdbj3T/VwjrwoWGPBWWuRGJ0meZ4Q0YVI6UQ4wpx7wEDMVYvGEL585HhYtV89NXhsORxd7SaZOZ9NMBHOg9G+W7BP4luG4J8mcZSGlfQ0PkqMJnMVGAKhR5j0kQLX7AAmaKHpk96pipDRIPKxy52DPU8t57ahU7hMIweXjedc6hRK0bkKuZSGA1w54ey8X3aDUoV3Y5DhnOnoAGfHrofvUZN+CYuTR3aIoNzPAXp9Zpnnz97KcpYwzWu1u6vahmwy+N
    - MIIDQzCCAiugAwIBAgIUDxHQ9JxxmnrLWTA5rAtIZCzY8mMwDQYJKoZIhvcNAQELBQAwKTEPMA0GA1UECgwGUmF0aWZ5MRYwFAYDVQQDDA1SYXRpZnkgU2FtcGxlMB4XDTIzMDYyOTA1MjgzMloXDTMzMDYyNjA1MjgzMlowKTEPMA0GA1UECgwGUmF0aWZ5MRYwFAYDVQQDDA1SYXRpZnkgU2FtcGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAshmsL2VM9ojhgTVUUuEsZro9jfI27VKZJ4naWSHJihmOki7IoZS83/3ATpkE1lGbduJ77M9UxQbEW1PnESB0bWtMQtjIbser3mFCn15yz4nBXiTIu/K4FYv6HVdc6/cds3jgfEFNw/8RVMBUGNUiSEWa1lV1zDM2v/8GekUr6SNvMyqtY8ooItwxfUvlhgMNlLgd96mVnnPVLmPkCmXFN9iBMhSce6sn6P9oDIB+pr1ZpE4F5bwagRBg2tWN3Tz9H/z2a51Xbn7hCT5OLBRlkorHJl2HKKRoXz1hBgR8xOL+zRySH9Qo3yx6WvluYDNfVbCREzKJf9fFiQeVe0EJOwIDAQABo2MwYTAdBgNVHQ4EFgQUKzciEKCDwPBn4I1YZ+sDdnxEir4wHwYDVR0jBBgwFoAUKzciEKCDwPBn4I1YZ+sDdnxEir4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAGh6duwc1MvV+PUYvIkDfgj158KtYX+bv4PmcV/aemQUoArqM1ECYFjtBlBVmTRJA0lijU5I0oZje80zW7P8M8pra0BM6x3cPnh/oZGrsuMizd4h5b5TnwuJhRvKFFUVeHn9kORbyQwRQ5SpL8cRGyYp+T6ncEmo0jdIOM5dgfdhwHgb+i3TejcF90sUs65zovUjv1wa11SqOdu12cCj/MYp+H8j2lpaLL2t0cbFJlBY6DNJgxr5qynccz8gbXrZmNbzC7W5QK5J7fcx6tlffOpt5cm427f9NiK2tira50HU7gC3HJkbiSTpXw10iXXMZzSbQ0/Hj2BF4B40WfAkgRg=
  io.cncf.notary.signingAgent: Notation/1.0.0
signature: As9aiY8rO7rQrZzQ7RU2yZ_uqX7wvdpW0kn0wuuxxzdSTA9sgltz2nymvj81FLB94uPDvoYj44_c3KF_OON1mD9HREdkwProy7i-b9gVcUw-aP3q_GhFQ8kBEjTIog2PkUexOO7v-JkAyrmDC6ZhxX8kzLyhZpMPtswtbWCpknKalxksFty2_Qwr3KcfZRnoPlISSSaAHRHK-cF8ABeMZNSKr2kWdLNhu6BDiO265MyittRvw4yex_rpVS9OBbQtMMPbJFxakyQeOPyfvYr4JykoazyWwKoT6pJqghJwbKgy6FM9rU94ZJnOly-GZHCok4RRrV31d8cAFNnw4CCp0A

署名の詳細は signature envelop の仕様に基づいて Base64 encode された形式で格納されているので、この中を見ていくと署名に関する情報が取得できます。例えば protected には 署名形式、署名時刻などの情報が記載されているので、base64 decode することで内容を確認できます。

$ cat output/blobs/sha256/74aaf3ca1c2baf0f0f878c8b4a470e208c46e04c29b36ad106e4523c5f648fc0 | yq -P ".protected" | base64 -d | yq -P
alg: PS256
crit:
  - io.cncf.notary.signingScheme
cty: application/vnd.cncf.notary.payload.v1+json
io.cncf.notary.signingScheme: notary.x509
io.cncf.notary.signingTime: "2023-06-29T07:01:15Z"

これより署名のスキーマが x509 であること、署名された日時が 2023-06-29T07:01:15Z であることがわかります。

header.x5c には署名に含まれる証明書の情報が記載されています。base64 decode すると pem 形式の証明書になるので、ファイルに書き出して openssl で中身を確認できます。

1 つ目の証明書
$ cat output/blobs/sha256/74aaf3ca1c2baf0f0f878c8b4a470e208c46e04c29b36ad106e4523c5f648fc0 | yq -P ".header.x5c[0]" | base64 -d > cert.crt
$ openssl x509 -in cert.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            36:fb:14:95:81:08:44:e8:82:18:06:7a:f3:cd:71:22:40:dc:62:34
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = Ratify, CN = Ratify Sample
        Validity
            Not Before: Jun 29 05:28:32 2023 GMT
            Not After : Jun 26 05:28:32 2033 GMT
        Subject: CN = ratify.default
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:f8:88:e1:2d:9c:c7:2a:e2:bb:f7:a2:39:0a:6e:
                    5d:44:ad:c3:b0:8b:3b:58:3c:09:54:59:3e:81:13:
                    20:c1:ca:51:bd:b0:83:ef:e5:09:a3:e0:8b:e2:68:
                    3b:03:f9:14:ee:fa:f7:f9:6a:2e:d6:0c:32:d0:fc:
                    14:f8:b4:cd:90:c8:a6:2d:34:f9:5c:d4:11:e0:3e:
                    48:a4:80:9e:61:3f:f9:33:05:a6:d9:f6:7a:37:71:
                    e8:3c:a5:97:50:a2:0d:5b:42:b1:82:26:f4:a7:61:
                    c9:19:8c:ed:62:be:67:e2:ae:87:a3:26:6c:1d:a4:
                    7b:96:39:bb:c7:48:63:b1:0a:d2:47:28:cd:4c:97:
                    ae:25:a9:f9:ba:62:0d:96:3a:1f:d0:57:30:98:62:
                    94:bb:5b:ac:9a:bd:d6:75:0c:12:3f:b0:2a:33:2b:
                    62:99:01:8a:67:aa:da:92:35:2c:d8:da:3e:75:2b:
                    9a:ae:0c:23:84:46:26:02:99:3d:a1:34:20:ed:93:
                    b0:83:a4:ae:b5:7a:62:c1:8b:5d:bd:d2:d0:7a:89:
                    1c:7a:5d:1b:f8:f1:b5:3a:23:1c:1a:9f:29:95:5d:
                    18:03:c2:a4:fc:35:96:23:5d:54:a0:cd:25:b9:fa:
                    47:15:1c:bc:2b:49:bc:fe:13:f0:33:f8:8f:41:e8:
                    f1:4d
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        11:6c:37:85:92:37:06:bd:97:47:d6:78:68:3a:62:0c:9c:06:
        c1:c7:fc:85:65:1e:1f:fc:09:2b:28:bb:78:91:17:e3:b6:66:
        ba:95:98:a6:e7:d9:0e:c6:ef:d0:57:6a:15:eb:93:cb:14:9f:
        56:56:34:26:32:61:23:18:87:5b:8f:74:ff:57:08:eb:c2:85:
        86:3c:15:96:b9:11:89:d2:67:99:e1:0d:18:54:8e:94:43:8c:
        29:c7:bc:04:0c:c5:58:bc:61:0b:e7:ce:47:85:8b:55:f3:d3:
        57:86:c3:91:c5:de:d2:69:93:99:f4:d3:01:1c:e8:3d:1b:e5:
        bb:04:fe:25:b8:6e:09:f2:67:19:48:69:5f:43:43:e4:a8:c2:
        67:31:51:80:2a:14:79:8f:49:10:2d:7e:c0:02:66:8a:1e:99:
        3d:ea:98:a9:0d:12:0f:2b:1c:b9:d8:33:d4:f2:de:7b:6a:15:
        3b:84:c2:30:79:78:de:75:ce:a1:44:ad:1b:90:ab:99:48:60:
        35:c3:9e:1e:cb:c5:f7:68:35:28:57:76:39:0e:19:ce:9e:80:
        06:7c:7a:e8:7e:f5:19:37:e0:98:b9:34:77:68:8a:0d:cc:f0:
        17:a7:d6:69:9e:7c:fd:ec:a7:29:63:0c:d6:bb:5b:ba:bd:a8:
        66:c3:2f:8d
2 つ目の証明書
$ cat output/blobs/sha256/74aaf3ca1c2baf0f0f878c8b4a470e208c46e04c29b36ad106e4523c5f648fc0 | yq -P ".header.x5c[1]" | base64 -d > ca.crt
$ openssl x509 -in ca.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0f:11:d0:f4:9c:71:9a:7a:cb:59:30:39:ac:0b:48:64:2c:d8:f2:63
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = Ratify, CN = Ratify Sample
        Validity
            Not Before: Jun 29 05:28:32 2023 GMT
            Not After : Jun 26 05:28:32 2033 GMT
        Subject: O = Ratify, CN = Ratify Sample
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:b2:19:ac:2f:65:4c:f6:88:e1:81:35:54:52:e1:
                    2c:66:ba:3d:8d:f2:36:ed:52:99:27:89:da:59:21:
                    c9:8a:19:8e:92:2e:c8:a1:94:bc:df:fd:c0:4e:99:
                    04:d6:51:9b:76:e2:7b:ec:cf:54:c5:06:c4:5b:53:
                    e7:11:20:74:6d:6b:4c:42:d8:c8:6e:c7:ab:de:61:
                    42:9f:5e:72:cf:89:c1:5e:24:c8:bb:f2:b8:15:8b:
                    fa:1d:57:5c:eb:f7:1d:b3:78:e0:7c:41:4d:c3:ff:
                    11:54:c0:54:18:d5:22:48:45:9a:d6:55:75:cc:33:
                    36:bf:ff:06:7a:45:2b:e9:23:6f:33:2a:ad:63:ca:
                    28:22:dc:31:7d:4b:e5:86:03:0d:94:b8:1d:f7:a9:
                    95:9e:73:d5:2e:63:e4:0a:65:c5:37:d8:81:32:14:
                    9c:7b:ab:27:e8:ff:68:0c:80:7e:a6:bd:59:a4:4e:
                    05:e5:bc:1a:81:10:60:da:d5:8d:dd:3c:fd:1f:fc:
                    f6:6b:9d:57:6e:7e:e1:09:3e:4e:2c:14:65:92:8a:
                    c7:26:5d:87:28:a4:68:5f:3d:61:06:04:7c:c4:e2:
                    fe:cd:1c:92:1f:d4:28:df:2c:7a:5a:f9:6e:60:33:
                    5f:55:b0:91:13:32:89:7f:d7:c5:89:07:95:7b:41:
                    09:3b
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                2B:37:22:10:A0:83:C0:F0:67:E0:8D:58:67:EB:03:76:7C:44:8A:BE
            X509v3 Authority Key Identifier:
                2B:37:22:10:A0:83:C0:F0:67:E0:8D:58:67:EB:03:76:7C:44:8A:BE
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Certificate Sign
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        68:7a:76:ec:1c:d4:cb:d5:f8:f5:18:bc:89:03:7e:08:f5:e7:
        c2:ad:61:7f:9b:bf:83:e6:71:5f:da:7a:64:14:a0:0a:ea:33:
        51:02:60:58:ed:06:50:55:99:34:49:03:49:62:8d:4e:48:d2:
        86:63:7b:cd:33:5b:b3:fc:33:ca:6b:6b:40:4c:eb:1d:dc:3e:
        78:7f:a1:91:ab:b2:e3:22:cd:de:21:e5:be:53:9f:0b:89:85:
        1b:ca:14:55:15:78:79:fd:90:e4:5b:c9:0c:11:43:94:a9:2f:
        c7:11:1b:26:29:f9:3e:a7:70:49:a8:d2:37:48:38:ce:5d:81:
        f7:61:c0:78:1b:fa:2d:d3:7a:37:05:f7:4b:14:b3:ae:73:a2:
        f5:23:bf:5c:1a:d7:54:aa:39:db:b5:d9:c0:a3:fc:c6:29:f8:
        7f:23:da:5a:5a:2c:bd:ad:d1:c6:c5:26:50:58:e8:33:49:83:
        1a:f9:ab:29:dc:73:3f:20:6d:7a:d9:98:d6:f3:0b:b5:b9:40:
        ae:49:ed:f7:31:ea:d9:5f:7c:ea:6d:e5:c9:b8:db:b7:fd:36:
        22:b6:b6:2a:da:e7:41:d4:ee:00:b7:1c:99:1b:89:24:e9:5f:
        0d:74:89:75:cc:67:34:9b:43:4f:c7:8f:60:45:e0:1e:34:59:
        f0:24:81:18

これを見ると Issuer: O = Ratify, CN = Ratify Sample によって発行された ratify の証明書が設定されていることがわかります。

  • 1 つ目: 2 つめの CA 証明書を issuer とする Subject: CN = ratify.default の証明書
  • 2 つ目: Issuer: O = Ratify, CN = Ratify Sample とする CA 証明書

以上をまとめると、対象イメージ ghcr.io/deislabs/ratify/notary-image:signed は Ratify の Issuer による証明書によって署名がされているということがわかりました。

なお notation inspect [image] を使うとより簡単にイメージの署名に関する情報を確認できます。

$ notation inspect ghcr.io/deislabs/ratify/notary-image:signed
Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:signed) because resolved digest may not point to the same signed artifact, as tags are mutable.
Inspecting all signatures for signed artifact
ghcr.io/deislabs/ratify/notary-image@sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
└── application/vnd.cncf.notary.signature
    └── sha256:57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e
        ├── media type: application/jose+json
        ├── signature algorithm: RSASSA-PSS-SHA-256
        ├── signed attributes
        │   ├── signingScheme: notary.x509
        │   └── signingTime: Thu Jun 29 07:01:15 2023
        ├── user defined attributes
        │   └── (empty)
        ├── unsigned attributes
        │   └── signingAgent: Notation/1.0.0
        ├── certificates
        │   ├── SHA256 fingerprint: af3390cf92e2077e02f6dcb531a8638241dfc0061851ca698f8c900ae79c1b68
        │   │   ├── issued to: CN=ratify.default
        │   │   ├── issued by: CN=Ratify Sample,O=Ratify
        │   │   └── expiry: Sun Jun 26 05:28:32 2033
        │   └── SHA256 fingerprint: 3b1393351a3ccd30caca18ecd4096c0591cd9c18feac6ea8d4c6ddbf543734f5
        │       ├── issued to: CN=Ratify Sample,O=Ratify
        │       ├── issued by: CN=Ratify Sample,O=Ratify
        │       └── expiry: Sun Jun 26 05:28:32 2033
        └── signed artifact
            ├── media type: application/vnd.docker.distribution.manifest.v2+json
            ├── digest: sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
            └── size: 942

notation inspect では Ratify の CA 証明書とこれで署名された証明書が 2 つ含まれること、署名の時刻、署名対象のアーティファクトの digest などの情報が簡単にわかるようになっています。

Ratify 側の検証設定

イメージが ratify の証明書によって署名されていることがわかったので、次に ratify がどのように署名を検証しているか見ていきます。

ratify では Verifier という CRD で署名の issuer を管理します。今回のセットアップではデフォルトで cosign, notation の issuer が作成されます。

$ k get verifiers.config.ratify.deislabs.io
NAME                ISSUCCESS   ERROR
verifier-cosign     true
verifier-notation   true

verifier-notation の中身は以下。

$ k get verifiers.config.ratify.deislabs.io verifier-notation -o yaml
apiVersion: config.ratify.deislabs.io/v1beta1
kind: Verifier
metadata:
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "5"
  creationTimestamp: "2025-04-24T14:59:10Z"
  generation: 1
  name: verifier-notation
  resourceVersion: "205595"
  uid: d5e8cda7-8ed4-4555-b52f-5358de53e16a
spec:
  artifactTypes: application/vnd.cncf.notary.signature
  name: notation
  parameters:
    trustPolicyDoc:
      trustPolicies:
      - name: default
        registryScopes:
        - '*'
        signatureVerification:
          level: strict
        trustStores:
        - ca:certs
        trustedIdentities:
        - '*'
      version: "1.0"
    verificationCertStores:
      certs:
      - ratify-notation-inline-cert-0
  version: 1.0.0
status:
  issuccess: true

これから、OCI アーティファクトの署名の artifactType が application/vnd.cncf.notary.signature である場合にはこの verify によって検証され、検証に使用される証明書は ratify-notation-inline-cert-0 に対応することがわかります。この証明書は keymanagementproviders CRD に保存されています。

$ k get keymanagementproviders.config.ratify.deislabs.io -o  yaml ratify-notation-inline-cert-0
apiVersion: config.ratify.deislabs.io/v1beta1
kind: KeyManagementProvider
metadata:
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "5"
  creationTimestamp: "2025-04-24T14:59:10Z"
  generation: 1
  name: ratify-notation-inline-cert-0
  resourceVersion: "205596"
  uid: 65ac626c-d057-4074-8cd6-ab8f78db837f
spec:
  parameters:
    contentType: certificate
    value: |
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
  refreshInterval: ""
  type: inline
status:
  issuccess: true
  lastfetchedtime: "2025-04-24T14:59:11Z"

spec.parameters.value の証明書は先程見た ratify の CA 証明書となっていて、実際にファイルを比較すると一致することがわかります。

$ k get keymanagementproviders.config.ratify.deislabs.io -o  yaml ratify-notation-inline-cert-0 | yq -r ".spec.parameters.value" > k8s-ca.crt
$ diff k8s-ca.crt ca.crt
$

なので、イメージの署名アーティファクトの type が artifactType: application/vnd.cncf.notary.signature の場合には上記の verify が検証に使用され、上記の CA 証明書を issuer とする証明書が署名アーティファクトに使用されていれば検証に成功する ことがわかりました。
これを踏まえて再度事前署名済みのイメージを見直すと、署名アーティファクトのタイプが application/vnd.cncf.notary.signature であり、fingerprint: af3390c の証明書が CA 証明書で署名されているので条件を満たすことがわかります。

$ notation inspect ghcr.io/deislabs/ratify/notary-image:signed

ghcr.io/deislabs/ratify/notary-image@sha256:8e3d01113285a0e4aa574da8eb9c0f112a1eb979d72f73399d7175ba3cdb1c1b
└── application/vnd.cncf.notary.signature
    └── sha256:57be2c1c3d9c23ef7c964bba05c7aa23b525732e9c9af9652654ccc3f4babb0e
        ├── certificates
        │   ├── SHA256 fingerprint: af3390cf92e2077e02f6dcb531a8638241dfc0061851ca698f8c900ae79c1b68
        │   │   ├── issued to: CN=ratify.default
        │   │   ├── issued by: CN=Ratify Sample,O=Ratify
        │   │   └── expiry: Sun Jun 26 05:28:32 2033
        │   └── SHA256 fingerprint: 3b1393351a3ccd30caca18ecd4096c0591cd9c18feac6ea8d4c6ddbf543734f5
        │       ├── issued to: CN=Ratify Sample,O=Ratify
        │       ├── issued by: CN=Ratify Sample,O=Ratify
        │       └── expiry: Sun Jun 26 05:28:32 2033

独自イメージに署名して検証する

イメージの署名アーティファクトの構造や ratify 側での検証の条件がだいたいわかったので、ここでは独自イメージに署名を行い、ratify を使って署名を検証する動作を確認します。

まず検証に使用するための適当なイメージとして dockerhub にある公式の nginx;latest イメージを利用します。
このイメージを指定した pod を起動しようとすると、イメージが署名されていないので当然起動しません。

$ k run demo --image=nginx:latest -n default
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [ratify-constraint] Subject failed verification: docker.io/library/nginx@sha256:5ed8fcc66f4ed123c1b2560ed708dc148755b6e4cbd8b943fab094f2c6bfa91e

ではこのイメージに署名していきます。イメージに署名するツールはいくつかありますが、ここでは先程使用した notation を使って署名につかう証明書の作成、署名を行います。
まず notation cert generate-test [cn_name] コマンドで自己署名用の CA と証明書をまとめて作成します。

$ notation cert generate-test ratify.my.test --default

これにより notation に CA 証明書と証明書が作成・登録されます。また、ローカルの ~/.config/notation に証明書と秘密鍵ファイルが保存されます。

# 登録された CA の確認
$ notation cert ls
STORE TYPE   STORE NAME       CERTIFICATE
ca           ratify.my.test   ratify.my.test.crt

# 登録された key (証明書) の確認
$ notation key ls
NAME               KEY PATH                                                     CERTIFICATE PATH                                             ID   PLUGIN NAME
* ratify.my.test   /home/ubuntu/.config/notation/localkeys/ratify.my.test.key   /home/ubuntu/.config/notation/localkeys/ratify.my.test.crt

# local path の確認
$ tree ~/.config/notation/
/home/ubuntu/.config/notation/
├── localkeys
│   ├── ratify.my.test.crt
│   └── ratify.my.test.key
├── signingkeys.json
└── truststore
    └── x509
        └── ca
            └── ratify.my.test
                └── ratify.my.test.crt

署名はコンテナレジストリ上のイメージに対して行われるので、nginx:latest をどこかのレジストリに push しておきます。ここでは harbor 上に harbor.centre.com/k8s/nginx-test:latest として push.

notation sign --key [cn] [image] で上記作成した key を使ってイメージに署名します。

$ notation sign --key ratify.my.test --signature-format cose harbor.centre.com/k8s/nginx-test:latest
Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:latest) because tags are mutable and a tag reference can point to a different artifact than the one signed.
Successfully signed harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde

notation ls [image] でレジストリ上のイメージに application/vnd.cncf.notary.signature で署名されたことが確認できます。

$ notation ls harbor.centre.com/k8s/nginx-test:latest
Warning: Always list the artifact using digest(@sha256:...) rather than a tag(:latest) because resolved digest may not point to the same signed artifact, as tags are mutable.
harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde
└── application/vnd.cncf.notary.signature
    └── sha256:101127119eb371aebecf92c48b7a8e62323347b8fe5b3fb6383327d8f2c4058a

ratify 側の設定

イメージへの署名は完了しましたが、イメージ署名元の CA ratify.my.test が ratify 側に登録されていないのでこの時点で対象イメージを使った pod を起動しようとしてもエラーとなります。
なので CA 証明書を追加するように verify CRD を編集します。直接編集してもいいですが、ここでは helm の values に書き換える方法で行うため values.yml に書き出します。

helm show values ratify/ratify > values.yml

CA 証明書は ~/.config/notation/truststore/x509/ca/ratify.my.test/ratify.my.test.crt に保存されているので中身を values.yml の notationCerts に追加します。クイックスタート時に指定した notation.crt も追加する場合は list で要素を追加します。

values.yml
notationCerts:
  - |
    -----BEGIN CERTIFICATE-----
    notation.crt の中身
    -----END CERTIFICATE-----
  - |
    -----BEGIN CERTIFICATE-----
    ratify.my.test.crt の中身
    -----END CERTIFICATE-----

最初に -set でしたオプションを反映させるために以下の部分も変更。

values.yml
policy:
  useRego: true # Set to true if Rego Policy would be used for evaluation.

featureFlags:
 # RATIFY_CERT_ROTATION enables rotation for TLS certificates.
  RATIFY_CERT_ROTATION: true

変更を適用

helm upgrade ratify ratify/ratify --values values.yml

これによって verify リソースに notation.crt と ratify.my.test.crt に対応する証明書が verificationCertStores.certs に追加されます。

$ k get verifiers.config.ratify.deislabs.io verifier-notation -o yaml
spec:
  parameters:
    verificationCertStores:
      certs:
      - ratify-notation-inline-cert-0
      - ratify-notation-inline-cert-1

証明書の実態は keymanagementproviders にそれぞれ保存されます。

$ k get keymanagementproviders.config.ratify.deislabs.io
NAME                            ISSUCCESS   ERROR   LASTFETCHEDTIME
ratify-notation-inline-cert-0   true                76s
ratify-notation-inline-cert-1   true                76s

また、pod 起動時のイメージ検証を行う際にコンテナレジストリ上のイメージに対してアクセスが発生しますが、レジストリが http/https のどちらで公開されているか、アクセスに認証が必要かによって必要な対応が異なります。

レジストリが HTTP の場合

コンテナレジストリが http の場合は store リソースの ORAS に useHttp: true を追加する必要があります(ないと HTTPS で通信しに行くのでエラーになる)。

$ k get stores.config.ratify.deislabs.io -o yaml
apiVersion: v1
items:
  apiVersion: config.ratify.deislabs.io/v1beta1
  kind: Store
  metadata:
    annotations:
      helm.sh/hook: pre-install,pre-upgrade
      helm.sh/hook-weight: "5"
    creationTimestamp: "2025-04-26T14:44:29Z"
    generation: 4
    name: store-oras
    resourceVersion: "647263"
    uid: fb8cd367-f425-4dce-bbfb-e608a0804750
  spec:
    name: oras
    parameters:
      cacheEnabled: true
      cosignEnabled: true
      ttl: 10
+      useHttp: true
    version: 1.0.0
  status:
    issuccess: true
kind: List
metadata:
  resourceVersion: ""

helm values 上で編集する場合は oras.useHttp を true に設定します。

values.yml
oras:
  useHttp: true

レジストリが HTTPS の場合

コンテナレジストリが自己署名証明書を使って HTTPS にしている場合には pod 作成時に Head \"https://harbor.centre.com/v2/k8s/nginx-test/manifests/latest\": tls: failed to verify certificate: x509: certificate signed by unknown authority などのエラーになるので追加の操作が必要になります。正式な手順かどうかは不明ですか以下の方法で対応できました。

コンテナレジストリの証明書に称されている CA 証明書を configmap に記載。

harbor-ca.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: harbor-ca
  namespace: gatekeeper-system
data:
  ca.crt: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----

Ratify pod で使用するように deployment に env, volume を追加。

env:
- name: SSL_CERT_FILE
  value: /etc/ssl/certs/harbor-ca.crt

volumeMounts:
- mountPath: /etc/ssl/certs/harbor-ca.crt
  name: harbor-ca
  readOnly: true
  subPath: ca.crt

volumes:
- configMap:
    name: harbor-ca
  name: harbor-ca

認証が必要な場合

コンテナレジストリのイメージを参照する際に認証が必要な場合、認証情報を使用するようにさらに追加の設定が必要です。

ORAS store では k8s の Docker Config Kubernetes secrets が利用できるのでこれを追加します。
まず普通にコンテナレジストリにログインした際の ~/.docker/config.json などの認証を確認します。docker login などでレジストリにログインした際に生成されるファイルを確認するのが楽ですが、中身は単にテキストファイルなので直接作っても ok.

docker-config
{
   "auths": {
        "harbor.centre.com": {
            "auth": "YWRt..."
        }
   }
}

これを指定した secret を作成。

$ kubectl create secret docker-registry harbor-credential -n gatekeeper-system \
    --from-file=.dockerconfigjson=./docker-config

values.yml で oras.authProviders.k8secretsEnabled: true に設定して helm upgrade を実行。

values.yml
oras:
  authProviders:
    k8secretsEnabled: true

現時点で helm values 内で SA を編集することはできないようなので、helm upgrade 後に以下のコマンドで PullSecrets に docker credential を使用するように更新。

kubectl patch serviceaccount ratify-admin -n gatekeeper-system -p '{"imagePullSecrets": [{"name": "harbor-credential"}]}'

これでコンテナレジストリ上のイメージを使用する pod を作成した際に上記の credential が利用されます。

まとめ

コンテナレジストリの構成によって必要な作業は異なりますが、必要な作業をまとめると以下のようになります。

  • notation 等でイメージに署名を行う
  • 署名に使用されている CA 証明書を ratify verify に登録する
  • (https の場合) ratify pod にレジストリの CA証明書を追加する
  • (認証が必要な場合) ratify ORAS store にレジストリ認証情報を追加する

ここまで作業すると、独自のイメージでも ratify の検証にパスして pod が起動できるようになります。

$ k run demo --image=harbor.centre.com/k8s/nginx-test:latest -n default
pod/demo created

ratify のログを見ると想定通り harbor.centre.com/k8s/nginx-test に対して ratify.my.test CN が設定されていて、isSuccess: true で検証にパスしていることがわかります。

ratify pod ログ
time=2025-04-26T16:45:44.936401719Z level=info msg=verification response for subject harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde:
{
  "version": "1.1.0",
  "isSuccess": true,
  "traceID": "6e8f6374-a5b3-4cca-b696-f5099f7ac529",
  "timestamp": "2025-04-26T16:45:44.936279253Z",
  "verifierReports": [
    {
      "subject": "harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde",
      "referenceDigest": "sha256:101127119eb371aebecf92c48b7a8e62323347b8fe5b3fb6383327d8f2c4058a",
      "artifactType": "application/vnd.cncf.notary.signature",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Notation signature verification success",
          "name": "verifier-notation",
          "verifierName": "verifier-notation",
          "type": "notation",
          "verifierType": "notation",
          "extensions": {
            "Issuer": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US",
            "SN": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US"
          }
        }
      ],
      "nestedReports": []
    }
  ]
} component-type=server go.version=go1.23.4 namespace= trace-id=6e8f6374-a5b3-4cca-b696-f5099f7ac529

脆弱性レポートを検証する

trivy などの image scanner ではイメージ内の脆弱性を含むパッケージを検査することができますが、OCI アーティファクトでは脆弱性検出結果レポートをアーティファクトとして含めることができます。ratify ではこの脆弱性レポートを検証し、イメージに検出結果レポートが含まれているか確認したり、脆弱性レポートが 24 時間以内に作成されているか、レポートに critical な CVE が含まれていないかなどの条件に基づいて検証を成功・失敗させたりできます。ratify で脆弱性レポートを検証する以下の手順に従って実際に動作を確認していきます。

https://ratify.dev/docs/plugins/verifier/vulnerabilityreport

脆弱性レポート検証を有効化するには helm values.yml の vulnerabilityreport に設定を追加して ratify を更新します。今回は以下の条件を設定します。

項目 説明
enabled true 脆弱性レポート検証を有効化する
maximumAge 24h レポートが 24 時間以内に作成されている場合のみ有効とする
disallowedSeverities high,critical レポートに high, critical の結果が含まれている場合は検証を失敗とする
values.yml
vulnerabilityreport:
  enabled: true
  passthrough: false
  schemaURL: ""
  createdAnnotationName: ""
  maximumAge: "24h"
  notaryProjectSignatureRequired: false
  disallowedSeverities:
    - "high"
    - "critical"
  denylistCVEs: []

これにより verify に verifier-vulnerabilityreport が追加されます。

$ k get verifiers.config.ratify.deislabs.io -o yaml verifier-vulnerabilityreport
apiVersion: config.ratify.deislabs.io/v1beta1
kind: Verifier
metadata:
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "5"
  creationTimestamp: "2025-04-27T10:53:54Z"
  generation: 1
  name: verifier-vulnerabilityreport
  resourceVersion: "834663"
  uid: 6a6a6db5-f46f-4969-96b0-355ee13d4e81
spec:
  artifactTypes: application/sarif+json
  name: vulnerabilityreport
  parameters:
    disallowedSeverities:
    - high
    - critical
    maximumAge: 24h
  version: 1.0.0
status:
  issuccess: true

イメージの署名と同様に ratify は脆弱性レポートの中身を見て検証の成功・失敗を判断するだけなので、検証失敗時に pod 起動を拒否するのは gatekeeper 側の設定となっています。以下のテンプレートをデプロイしてこれらの設定を適用します。

k apply -f https://raw.githubusercontent.com/deislabs/ratify/v1.4.0/library/vulnerability-report-validation/template.yaml
k apply -f https://raw.githubusercontent.com/deislabs/ratify/v1.4.0/library/vulnerability-report-validation/samples/constraint.yaml

先程起動に成功したイメージを使って pod を起動しようとすると、脆弱性レポートがイメージに含まれていないので起動に失敗します。

$ k run demo --image=harbor.centre.com/k8s/nginx-test:latest -n default
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [vulnerability-report-validation-constraint] Subject failed vulnerability report verification: harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde

では trivy を使ってこのイメージに対する脆弱性レポートを見てみます。

$ trivy image harbor.centre.com/k8s/nginx-test:latest

Report Summary

┌────────────────────────────────────────────────────────┬────────┬─────────────────┬─────────┐
│                         Target                         │  Type  │ Vulnerabilities │ Secrets │
├────────────────────────────────────────────────────────┼────────┼─────────────────┼─────────┤
│ harbor.centre.com/k8s/nginx-test:latest (debian 12.10) │ debian │       156       │    -    │
└────────────────────────────────────────────────────────┴────────┴─────────────────┴─────────┘

harbor.centre.com/k8s/nginx-test:latest (debian 12.10)
======================================================
Total: 156 (UNKNOWN: 2, LOW: 99, MEDIUM: 40, HIGH: 13, CRITICAL: 2)

このイメージには CVE が 158 件、critical や high も含まれていますが、ひとまず脆弱性レポートをイメージに添付します。ratify が受け付けるレポートのスキーマはデフォルトで sarif なので -f sarif の指定が必要。

# sarif 形式のレポートを作成
$ trivy -q -f sarif image harbor.centre.com/k8s/nginx-test:latest > trivy-sarif.json

# harbor.centre.com/k8s/nginx-test:latest にレポートを添付
$ oras attach \
    --artifact-type application/sarif+json \
    --annotation "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
    harbor.centre.com/k8s/nginx-test:latest \
    trivy-sarif.json

oras discover [image] で notary の署名に加えて脆弱性レポートが追加されたことが確認できます。

$ oras discover harbor.centre.com/k8s/nginx-test:latest
harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde
├── application/vnd.cncf.notary.signature
│   └── sha256:101127119eb371aebecf92c48b7a8e62323347b8fe5b3fb6383327d8f2c4058a
└── application/sarif+json
    └── sha256:510fc8921721f01648556e09b9d7ac2416cd81af5182b2f0735c2b82c499edfb

24 時間以内に作成された脆弱性レポートがイメージに添付されているという条件は満たしてますが、レポートに critical, high の CVE が含まれているため相変わらず pod 起動は拒否されます。

$ k run demo --image=harbor.centre.com/k8s/nginx-test:latest -n default
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [ratify-constraint] Subject failed verification: harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde
[vulnerability-report-validation-constraint] Subject failed vulnerability report verification: harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde

ratify pod ログを見るとどのような CVE で弾かれているか確認できます。

ratify pod ログ
time=2025-04-27T05:40:50.783384786Z level=info msg=verification response for subject harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde:
{
  "version": "1.1.0",
  "isSuccess": false,
  "traceID": "10440614-e000-4c41-a857-45d15ac8fa22",
  "timestamp": "2025-04-27T05:40:50.783359087Z",
  "verifierReports": [
    {
      "subject": "harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde",
      "referenceDigest": "sha256:101127119eb371aebecf92c48b7a8e62323347b8fe5b3fb6383327d8f2c4058a",
      "artifactType": "application/vnd.cncf.notary.signature",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Notation signature verification success",
          "name": "verifier-notation",
          "verifierName": "verifier-notation",
          "type": "notation",
          "verifierType": "notation",
          "extensions": {
            "Issuer": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US",
            "SN": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US"
          }
        }
      ],
      "nestedReports": []
    },
    {
      "subject": "harbor.centre.com/k8s/nginx-test@sha256:ae61960a9324ea2071d81ef4554f9015ff89cfa43cacbfe8b4a23d7d3710ecde",
      "referenceDigest": "sha256:510fc8921721f01648556e09b9d7ac2416cd81af5182b2f0735c2b82c499edfb",
      "artifactType": "application/sarif+json",
      "verifierReports": [
        {
          "isSuccess": false,
          "message": "Found disallowed severities. See extensions field for details.",
          "name": "verifier-vulnerabilityreport",
          "verifierName": "verifier-vulnerabilityreport",
          "type": "vulnerabilityreport",
          "verifierType": "vulnerabilityreport",
          "extensions": {
            "createdAt": "2025-04-27T05:38:13Z",
            "disallowedSeverities": [
              "high",
              "critical"
            ],
            "scanner": "trivy",
            "severityViolations": {
              "CVE-2023-2953": "high",
              "CVE-2023-31484": "high",
              "CVE-2023-39616": "high",
              "CVE-2023-45853": "critical",
              "CVE-2023-52355": "high",
              "CVE-2023-52425": "high",
              "CVE-2023-6879": "critical",
              "CVE-2024-25062": "high",
              "CVE-2024-56171": "high",
              "CVE-2024-56406": "high",
              "CVE-2024-8176": "high",
              "CVE-2025-24928": "high",
              "CVE-2025-27113": "high",
              "CVE-2025-32414": "high",
              "CVE-2025-32415": "high"
            }
          }
        }
      ],
      "nestedReports": []
    }
  ]
} component-type=server go.version=go1.23.4 namespace= trace-id=10440614-e000-4c41-a857-45d15ac8fa22

次に critical, high の CVE を修正して pod が正常に起動する動作を確認しますが、このイメージだと脆弱性が多く対応が面倒なので代わりに python:3.9.22-alpine3.21 に切り替えます。

$ trivy image  python:3.9.22-alpine3.21

Python (python-pkg)

Total: 3 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 0)

┌───────────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────┐
│        Library        │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                          Title                           │
├───────────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤
│ pip (METADATA)        │ CVE-2023-5752  │ MEDIUM   │ fixed  │ 23.0.1            │ 23.3          │ pip: Mercurial configuration injectable in repo revision │
│                       │                │          │        │                   │               │ when installing via pip                                  │
│                       │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2023-5752                │
├───────────────────────┼────────────────┼──────────┤        ├───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤
│ setuptools (METADATA) │ CVE-2022-40897 │ HIGH     │        │ 58.1.0            │ 65.5.1        │ pypa-setuptools: Regular Expression Denial of Service    │
│                       │                │          │        │                   │               │ (ReDoS) in package_index.py                              │
│                       │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2022-40897               │
│                       ├────────────────┤          │        │                   ├───────────────┼──────────────────────────────────────────────────────────┤
│                       │ CVE-2024-6345  │          │        │                   │ 70.0.0        │ pypa/setuptools: Remote code execution via download      │
│                       │                │          │        │                   │               │ functions in the package_index module in...              │
│                       │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2024-6345                │
└───────────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────┘

このイメージでは high が 2 件で setuptools を 70.0.0 以上に上げれば解決できるので、新しいイメージを作成。

Dockerfile
FROM python:3.9.22-alpine3.21

RUN pip install --upgrade setuptools

ビルドして harbor に push し、脆弱性レポートを追加。

$ docker build -t python:fixed .
$ docker tag python:fixed harbor.centre.com/k8s/python:fixed
$ docker push harbor.centre.com/k8s/python:fixed
$ trivy image harbor.centre.com/k8s/python:fixed -f sarif > res.json
$ oras attach \
    --artifact-type application/sarif+json \
    --annotation "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
    harbor.centre.com/k8s/python:fixed \
    res.json

$ oras discover harbor.centre.com/k8s/python:fixed
harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d
└── application/sarif+json
    └── sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f

これで ratify の検証が成功するので本来は pod が起動するようになるはずですが、手元で試すと検証に成功しているにも関わらずエラーが解消されませんでした。

$ k run demo --image=harbor.centre.com/k8s/python:fixed -n default
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [vulnerability-report-validation-constraint] Subject failed vulnerability report verification: harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d

ratify pod ログではトップレベルで isSuccess": true になっているため検証に成功していることが確認できます。

time=2025-04-27T16:30:46.505107331Z level=info msg=verification response for subject harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d:
{
  "version": "1.1.0",
  "isSuccess": true,
  "traceID": "6ca8434b-832f-4632-8d35-e67eca99230f",
  "timestamp": "2025-04-27T16:30:46.505091771Z",
  "verifierReports": [
    {
      "subject": "harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d",
      "referenceDigest": "sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f",
      "artifactType": "application/sarif+json",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Validation succeeded",
          "name": "verifier-vulnerabilityreport",
          "verifierName": "verifier-vulnerabilityreport",
          "type": "vulnerabilityreport",
          "verifierType": "vulnerabilityreport",
          "extensions": {
            "createdAt": "2025-04-27T16:29:27Z",
            "scanner": "trivy"
          }
        }
      ],
      "nestedReports": []
    }
  ]
} component-type=server go.version=go1.23.4 namespace= trace-id=6ca8434b-832f-4632-8d35-e67eca99230f

いろいろ constrainttemplates をコメントアウトしたりしているとエラーが解消されて pod が起動するようになったため、ratify の検証自体は成功しているものの gatekeeper の Rego policy 側で検証結果をうまくチェックできずにエラーが発生しているようです。

脆弱性レポートへの署名

一番始めの例ではイメージのマニフェスト自体が notation で署名されていましたが、OCI アーティファクトではイメージに関連する脆弱性レポートや SBOM をアーティファクトとして含めることができ、そのアーティファクトに対してさらに署名 (signature) を設定することができます。


アーティファクトの参照関係。https://ratify.dev/docs/concepts/ratify-framework-overview/ より引用

Vulnerability Report with Signature Validation によると notaryProjectSignatureRequired というプロパティがありますが、これはイメージに追加された脆弱性レポート自体に署名が設定されているかどうか検証する項目になっています。

レポートへの署名はイメージへの署名と同様にレポートの digest を指定して notation sign を実行すれば ok です。

notation sign --key ratify.my.test --signature-format cose harbor.centre.com/k8s/python@sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f
Successfully signed harbor.centre.com/k8s/python@sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f

これにより脆弱性レポートの下に署名が追加されます。

$ oras discover harbor.centre.com/k8s/python:fixed
harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d
└── application/sarif+json
    └── sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f
        └── application/vnd.cncf.notary.signature
            └── sha256:6854a11632023acaa67d4e473c875d5285990eb6b575a4c6591fcdc2fab8f6db

イメージのマニフェストにも署名を追加すると、最終的なアーティファクトの tree 構造は以下のようになります。

$ notation sign --key ratify.my.test --signature-format cose harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d
Successfully signed harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d


$ oras discover harbor.centre.com/k8s/python:fixed
harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d
├── application/sarif+json
│   └── sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f
│       └── application/vnd.cncf.notary.signature
│           └── sha256:6854a11632023acaa67d4e473c875d5285990eb6b575a4c6591fcdc2fab8f6db
└── application/vnd.cncf.notary.signature
    └── sha256:2919dc1251afc432fec32b02e74fee1b9d43516b666a299f26d72165d37ddb1a

このイメージを ratify で検証すると脆弱性レポート "artifactType": "application/sarif+json の下に nestedReports として signature が追加され、それに対しても検証が行われるようになります。

ratify 検証結果
{
  "version": "1.1.0",
  "isSuccess": true,
  "traceID": "cc24f460-4efd-4f71-afdb-a30207ca7909",
  "timestamp": "2025-04-27T17:05:12.028612684Z",
  "verifierReports": [
    {
      "subject": "harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d",
      "referenceDigest": "sha256:2919dc1251afc432fec32b02e74fee1b9d43516b666a299f26d72165d37ddb1a",
      "artifactType": "application/vnd.cncf.notary.signature",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Notation signature verification success",
          "name": "verifier-notation",
          "verifierName": "verifier-notation",
          "type": "notation",
          "verifierType": "notation",
          "extensions": {
            "Issuer": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US",
            "SN": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US"
          }
        }
      ],
      "nestedReports": []
    },
    {
      "subject": "harbor.centre.com/k8s/python@sha256:5e5bf077802e80bff5aab420823d4097b13a48084132bda49ee932e28e1d157d",
      "referenceDigest": "sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f",
      "artifactType": "application/sarif+json",
      "verifierReports": [
        {
          "isSuccess": true,
          "message": "Validation succeeded",
          "name": "verifier-vulnerabilityreport",
          "verifierName": "verifier-vulnerabilityreport",
          "type": "vulnerabilityreport",
          "verifierType": "vulnerabilityreport",
          "extensions": {
            "createdAt": "2025-04-27T16:29:27Z",
            "scanner": "trivy"
          }
        }
      ],
      "nestedReports": [
        {
          "subject": "harbor.centre.com/k8s/python@sha256:e7fa4017cb0a7a2178ebbf16003e58763c0b6e655ef166db53a5078ea9cad15f",
          "referenceDigest": "sha256:6854a11632023acaa67d4e473c875d5285990eb6b575a4c6591fcdc2fab8f6db",
          "artifactType": "application/vnd.cncf.notary.signature",
          "verifierReports": [
            {
              "isSuccess": true,
              "message": "Notation signature verification success",
              "name": "verifier-notation",
              "verifierName": "verifier-notation",
              "type": "notation",
              "verifierType": "notation",
              "extensions": {
                "Issuer": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US",
                "SN": "CN=ratify.my.test,O=Notary,L=Seattle,ST=WA,C=US"
              }
            }
          ],
          "nestedReports": []
        }
      ]
    }
  ]
}

その他の機能

その他の機能などの簡単なまとめ

  • パブリッククラウドでの使用
  • SBOM の検証
    • イメージに software bill of material (SOM) アーティファクトを添付し、それを ratify で検証する
  • Ratify CLI
    • ratify を cli で使える。手元での検証などに有効そう
  • プライグインの開発
    • プライグインを自作することでデフォルトでサポートしていないタイプのアーティファクトを検証したりできる
  • ratify メトリクス
    • ratify で取得可能なメトリクスの一覧。prometheus で収集して grafana などで可視化できる。
  • HA 構成
    • darp, redis と組み合わせることで複数の ratify pod でリクエストを処理する。
  • Gatekeeper policy の作成
    • gatekeeper の rego policy を作成する際のガイド
    • Rego template にもある
    • ratify -> gatekeeper のレスポンスのフォーマットは Verification Response にある

Discussion