本記事では、KubernetesにおけるSigstoreプロジェクトの活用方法を解説します。
Sigstoreの概要や導入事例、キーレス署名については、前回の記事[Cosignによる署名検証とSigstoreの全体像]をあわせてご覧ください。
Cosignのインストール
Sigstoreエコシステムにおける署名アーティファクトの検証には、Cosignのみが必要です。インストール方法はいくつかあリますが、詳細は公式ドキュメントを参照してください。ここでは、例としてGoを用いたインストール方法を紹介します。
$ go install github.com/sigstore/cosign/v2/cmd/cosign@latest
$ cosign --help
Kubernetesアーティファクトの検証
Kubernetesアーティファクトの署名と検証は、KEP-3031: Signing release artifacts で進められています。v.1.24 で α、v.1.26 で β になっています。β では、Kubernetes標準のアーティファクト(バイナリやコンテナイメージ)に署名されており、全てのアーティファクトが署名されるようになった段階で、GA に到達する予定です。
この機能により、Kubernetesユーザはリリースプロセスの一部として署名検証を組み込み、サプライチェーン攻撃のリスクを軽減できます。ここでいう「Kubernetesアーティファクト」とは、PodやDeploymentなどのリソースではなく、Kubernetes本体を構成する主要コンポーネントであり、kube-apiserver
やkubectl
といったバイナリ、公式のコンテナイメージ、リリースに含まれる関連ファイルなどです(KEP-3031では「リリースアーティファクト」と呼ばれています)。
バイナリ署名の検証
Kubernetesのリリースでは、すべてのバイナリに対して Cosign によるキーレス署名が付与されています。
ここでは、Kubernetesのダウンロードページ から kubectl
をダウンロードし、Cosignを用いて正規のバイナリであるかを検証します。
kubectl
のバイナリ・署名・証明書をダウンロード
$ curl -OL https://dl.k8s.io/v1.33.3/bin/linux/amd64/kubectl
$ curl -OL https://dl.k8s.io/v1.33.3/bin/linux/amd64/kubectl.sig
$ curl -OL https://dl.k8s.io/v1.33.3/bin/linux/amd64/kubectl.cert
バイナリの署名検証 バイナリ署名の署名情報は以下の通りです。
- Identity:
krel-staging@k8s-releng-prod.iam.gserviceaccount.com
- OIDC Issuer:
https://accounts.google.com
$ cosign verify-blob kubectl \
--signature kubectl.sig \
--certificate kubectl.cert \
--certificate-identity krel-staging@k8s-releng-prod.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
Verified OK
KuberentesコンポーネントのバイナリはすべてSHA256チェックサムを公開されています。そのため、ハッシュ値による検証も可能です。
$ curl -L https://dl.k8s.io/v1.33.3/bin/linux/amd64/kubectl.sha256
$ curl -OL https://dl.k8s.io/v1.33.3/bin/linux/amd64/kubectl
$ sha256sum kubectl
ハッシュ値による検証はファイルの完全性(改竄されていないこと)は確認できますが、誰がビルドしたかまでは証明できません。もし、ダウンロードページに置かれたバイナリとハッシュが両方とも改竄されてしまえば、攻撃を防ぐことはできません。
一方で Cosign による検証では、完全性に加えて署名者の正当性も確認できます。Fulcio と Rekor により、どこでビルドされたのかまで追跡することが可能になります。
コンテナイメージの署名検証
コンテナイメージにも Cosign のキーレス署名が付与されています。Kubernetesアーティファクトのコンテナイメージは、registry.k8s.io
というコンテナレジストリに保存されています。
コンテナイメージの場合には、コンテナレジストリに保存したコンテナイメージと署名、証明書を使用するため、バイナリや署名、証明書といったファイルをダウンロードする必要はありません。仕組みについては、前回の記事を参照してください。
cosign verify
でコンテナイメージの検証 コンテナイメージの署名情報は以下の通りです。
- Identity:
krel-trust@k8s-releng-prod.iam.gserviceaccount.com
- OIDC Issuer:
https://accounts.google.com
$ cosign verify registry.k8s.io/kube-apiserver-amd64:v1.33.3 \
--certificate-identity krel-trust@k8s-releng-prod.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com \
| jq .
[
{
"critical": {
"identity": {
"docker-reference": "registry.k8s.io/kube-apiserver-amd64"
},
"image": {
"docker-manifest-digest": "sha256:cc86c18b1a7282d2640dbe74fb65aa9ddfcc677190379d39fa4b91057405ae66"
},
},
"optional": {
"Issuer": "https://accounts.google.com",
"Subject": "krel-trust@k8s-releng-prod.iam.gserviceaccount.com",
}
}
]
この結果から、registry.k8s.io/kube-apiserver-amd64:v1.33.3 が krel-trust@k8s-releng-prod.iam.gserviceaccount.com
によって署名されていることを確認できます。
コンテナイメージを利用する際のベストプラクティスとして、コンテナイメージのハッシュ値を指定することが推奨されています。Podマニフェストで、 registry.k8s.io/kube-apiserver-amd64@sha256:cc86c18b1a7282d2640dbe74fb65aa9ddfcc677190379d39fa4b91057405ae66
のように指定することで、確実に署名されたコンテナを使用することができます。
Kubernetesでは、署名済みのコンテナイメージ一覧を SPDX 2.3 形式の SBOM として公開しています。これを利用することで、全てのコアコンポーネントをまとめて検証することが可能です。
以下のように SBOM からイメージ一覧を抽出し、一括検証することができます。
# 全てのコンテナイメージを取得
$ curl -Ls "https://sbom.k8s.io/$(curl -Ls https://dl.k8s.io/release/stable.txt)/release" \
| grep "SPDXID: SPDXRef-Package-registry.k8s.io" \
| grep -v sha256 | cut -d- -f3- | sed 's/-/\//' | sed 's/-v1/:v1/' \
| sort > images.txt
# 各イメージを Cosign で検証
$ while IFS= read -r image
do
cosign verify "$image" \
--certificate-identity krel-trust@k8s-releng-prod.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com \
| jq .
done < images.txt
ただし注意点として、署名の検証が常に成功するとは限りません。
Kubernetes の公式レジストリ registry.k8s.io
はマルチベンダー・マルチリージョン構成をとっており、環境によっては同一のイメージでも署名情報が一致しないことがあります。
実際に、リージョンによる不一致や一部パッチリリースで署名が付与されていないといった問題が報告されています。
- https://github.com/kubernetes/registry.k8s.io/issues/187
- https://github.com/kubernetes/release/issues/2962
- https://github.com/kubernetes/kubernetes/issues/129199
アドミッションコントローラーによる署名検証
Sigstore には、Kubernetesのアドミッションコントローラーとして policy-controller が用意されています。これは、ValidatingAdmissionWebhook を利用して動作し、Podの作成や更新時に cosign verify
や cosign verify-attestation
を実行し、その結果に基づいてリソースの許可/拒否を判断します。
policy-controller を導入することで、Cosign から取得したサプライチェーンメタデータに基づいてクラスター内でポリシーを強制できます。具体的には、コンテナイメージの署名やアテステーションを検証し、アテステーションに対してcueまたはregoで定義したポリシーを適用します。
ポリシーはネームスペース単位で設定でき、複数のポリシーを同時に適用することも可能です。
policy-controller はコンテナイメージのタグをdigestに解決することで、署名検証後にタグが差し替えられて別のイメージが実行されることを防ぎます。

policy-controllerのインストール
既存のKubernetesクラスターを利用する場合はこの手順は不要です。ここでは検証用として、kindを使い、コントロールプレーン1台・ワーカー1台の2ノード構成でクラスターを作成します。
$ kind create cluster --config - <<EOF
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
- role: worker
EOF
policy-controller は Helmチャート で配布されています。インストール方法は、Artifact HUBのpolicy-controller が参考になります。
$ helm repo add sigstore https://sigstore.github.io/helm-charts
$ helm repo update
$ kubectl create namespace cosign-system
$ helm install policy-controller -n cosign-system sigstore/policy-controller
インストール後、policy.sigstore.devというValidatingWebhookConfigurationが作られており、policy-controller-webhookというPodがRunningになっていれば、準備完了です。
policy-controllerの有効化
デフォルトでは、opt-in 形式になっています。これは、既存環境に影響を与えないようにするためです。
そのため、Namespace に policy.sigstore.dev/include=true
というラベルを付与した場合のみ、その Namespace 内の Pod が検証対象になります。 今回は、ns1
とns2
というNamespaceを作成し、ns1
のみ policy-controller を有効化します。
$ kubectl create namespace ns1
$ kubectl create namespace ns2
$ kubectl label namespace ns1 policy.sigstore.dev/include=true
この後、Keyless署名の検証用に、Cosignで署名したコンテナイメージ mozsec/hello を使います。Cosign によるコンテナイメージ署名の手順については、前回の記事を参照してください。
それでは、ns1
と ns2
にそれぞれ mozsec/hello を使った Pod を作成してみます。ns2
では Pod が作られるのに対して、ns1
では、”policy.sigstore.dev” でリクエストが拒否されていることが確認できます。
$ kubectl run -n ns1 --image mozsec/hello:latest hello
Error from server (BadRequest): admission webhook "policy.sigstore.dev" denied the request: validation failed: no matching policies: spec.containers[0].image
index.docker.io/mozsec/hello:latest@sha256:714afd8d69161a880ca9695dfa9843784e0ffe550d56e3c9c66af4db04ad8bae
$ kubectl run -n ns2 --image mozsec/hello:latest hello
pod/hello created
ClusterImagePolicyの定義
ns1
でリクエストが拒否されているのは、署名を検証するポリシーが未定義のためです。policy-controllerはポリシーが存在しない場合、デフォルトで拒否します。
ポリシーは、ClusterImagePolicy
リソースで定義します。
images.glob で検証対象のコンテナイメージを指定します。コンテナイメージの指定には、GolangのMatchが使われており、ホストが省略されると index.docker.io がデフォルト値として使用されます。
キーレス署名の場合、authorities.keylessで誰の署名を許可するか指定します。issuerで OIDC IdP を指定し、subject で署名者を指定します。subject は完全一致であるのに対して、subjectRegExpでは正規表現で一致するか判定します。少なくとも1つの authority に一致すれば、そのイメージは許可されます。
$ kubectl apply -f - <<EOF
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: mozsec-policy
spec:
images:
- glob: "index.docker.io/mozsec/*" # 全てのコンテナイメージを指定するには "**"
authorities:
- keyless:
identities:
- issuer: https://github.com/login/oauth
subject: m0253c@gmail.com # 署名者(完全一致)
- issuer: https://accounts.google.com
subjectRegExp: ^[a-z]+@3-shake\.com$ # 組織のメールだけ許可
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: ^https://github\.com/your-org/.+/.github/workflows/.+@refs/heads/main$
# your-orgのリポジトリのmainブランチで実行されたWorkflowの署名のみ許可
EOF
それでは、再度 ns1
で Pod を起動してみます。mozsec/hello イメージは GitHubアカウントm0253c@gmail.com によって署名されているため、Pod は正常に作成されます。
$ kubectl run -n ns1 --image mozsec/hello:latest hello
pod/hello created
一方で、nginx の Pod を起動しようとすると、リクエストが拒否されます。これは、nginx イメージに m0253c@gmail.com の署名が存在しないためです。
$ kubectl run -n ns1 --image nginx:latest nginx
Error from server (BadRequest): admission webhook "policy.sigstore.dev" denied the request: validation failed: failed policy: mozsec-policy: spec.containers[0].image
index.docker.io/library/nginx:latest@sha256:33e0bbc7ca9ecf108140af6288c7c9d1ecc77548cbfd3952fd8466a75edefe57 signature keyless validation failed for authority authority-0 for index.docker.io/library/nginx@sha256:33e0bbc7ca9ecf108140af6288c7c9d1ecc77548cbfd3952fd8466a75edefe57: no signatures found
どのポリシーにもマッチしなかったとき
policy-controller では、コンテナイメージに一致するポリシーが存在しない場合の挙動を制御できます。
config-policy-controller
ConfigMapで no-match-policy を設定し、その値を以下から選びます。
- deny(デフォルト): ポリシーに一致しないイメージは拒否
- warn: Pod は作成されるが、警告が出力
- allow: ポリシーに一致しなくても許可
これにより、先ほど ClusterImagePolicy が未設定の状態でリクエストが拒否されたケースも、no-match-policy を設定することで、「警告だけを出す」または「許可する」といった挙動に変更可能です。
opt-out 形式に変更
デフォルトでは opt-in 形式 であり、Namespace にラベル policy.sigstore.dev/include=true
を付けた場合のみ、コンテナイメージの検証が有効になります。
一方で、opt-out 形式 に切り替えることも可能です。これは「すべての Namespace で検証を有効にし、特定の Namespace のみ除外する」方式です。この方法を採用すると、ポリシーの設定漏れを防ぎやすくなりますが、既存 Pod に影響を与えるリスクがあります。 特に kube-system
Namespace を除外しない場合、コアコンポーネント(kube-apiserver や kube-scheduler など)が拒否され、クラスタが正常に動作しなくなる恐れがあります。
opt-out 形式への切り替えは、MutatingWebhookConfiguration の namespaceSelector を修正することで行えます。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
webhooks:
- name: policy.sigstore.dev
namespaceSelector:
matchExpressions:
- key: policy.sigstore.dev/exclude
operator: DoesNotExist
この設定では、デフォルトで全 Namespace が検証対象となり、policy.sigstore.dev/exclude=true
ラベルを付与した Namespace のみ検証対象から除外されます。
YAMLマニフェストの署名・検証
Sigstoreには k8s-manifest-sigstore というkubectlのプラグインも用意されています。このプラグインを使用すると、Kubernetesの YAMLマニフェスト に署名を付与し、デプロイメントチームはその信頼性を検証できます。つまり「コンテナイメージの署名検証」に加えて、「マニフェスト自体の整合性検証」も可能にする試みです。
ただし、まだ開発中のプロジェクトであり、本番環境での利用は推奨されていません。
k8s-manifest-sigstoreのインストール
このプラグインはバイナリとして提供されており、PATH が通ったディレクトリに配置するだけで、kubectl サブコマンドとして利用できます。
$ go install github.com/sigstore/k8s-manifest-sigstore/cmd/kubectl-sigstore@latest
$ kubectl sigstore --help
マニフェストの署名
kubectl sigstore sign
を実行すると、YAML マニフェストを OCI イメージに変換してレジストリにプッシュし、Cosign で署名します。
流れとしては「イメージ署名」と同様ですが、対象が YAML マニフェストになります。
ここでは、nginx-pod.yaml を Docker Hub にプッシュした後、署名しています。mozsec/bundle-bar を見ると、devタグがついたアーティファクトとその署名(.sigサフィックス)が確認できます。
# nginx Pod を起動するマニフェストの作成
$ kubectl run nginx --image=nginx --dry-run=client -o yaml > nginx-pod.yaml
# マニフェストの署名
$ kubectl sigstore sign -f nginx-pod.yaml --image docker.io/mozsec/bundle-bar:dev
マニフェストの検証
ローカルのマニフェストファイルを、OCIレジストリに保存された署名で検証できます。
$ kubectl sigstore verify -f nginx-pod.yaml -i docker.io/mozsec/bundle-bar:dev
さらに、実際にデプロイされたリソースから YAML を取得し、その内容を検証することも可能です。ただし、spec.nodeName
などスケジューリング後に付与されるフィールドは差分として検出されてしまいます。この問題を回避するには、config.yaml のように無視するフィールドを定義する必要があります。
以下の例では、 ノード名やボリューム名などスケジューリング後に付与されるフィールドを無視する設定を加えて検証しています。
$ cat ignore-config.yaml
ignoreFields: # 動的なフィールドを無視する
- objects:
- kind: Pod
name: nginx
namespace: ns2
fields:
- spec.nodeName
- spec.volumes.*.name
- spec.containers.*.volumeMounts.*.name
$ kubectl sigstore verify-resource pods nginx -i docker.io/mozsec/bundle-bar:dev \
-c ignore-config.yaml
このように無視フィールドを設定すれば差分は解消できますが、対象ごとに個別設定が必要になるため、現時点では運用面でのハードルはやや高いといえます。
まとめ
本記事では、KEP-3031に基づく公式アーティファクト検証、Cosignによるイメージ署名検証、policy-controllerによるポリシー強制、そしてマニフェスト署名の試行までを解説しました。
前回に引き続き、本記事では Keyless 署名を中心に解説しましたが、Cosign は従来の PGP/GPG 鍵 や キーペア(x509)・KMS(AWS/GCP/Azure/HSM) を用いた署名・検証にも対応しています。
さらに詳しく知りたい方は、公式ドキュメントをご覧ください。
参考
- [1] Kubernetes Documentation, Verify Signed Kubernetes Artifacts
https://kubernetes.io/docs/tasks/administer-cluster/verify-signed-artifacts/ - [2] KEP-3031: Signing release artifacts
https://github.com/kubernetes/enhancements/tree/abe9a2b99278b3667de8df7166995376cc7a8971/keps/sig-release/3031-signing-release-artifacts - [3] ArtifactHUB policy-controller
https://artifacthub.io/packages/helm/sigstore/policy-controller - [4] Sigstore Helm Repository
https://sigstore.github.io/helm-charts/ - [5] Sitstore Kubernetes Policy Controller
https://docs.sigstore.dev/policy-controller/overview/#configuring-keyless-authorities - [6] SigstoreによるKubernetesマニフェストの署名と検証
https://qiita.com/reoring/items/1ce8a19586333f263125 - [7] k8s-manifest-sigstore
https://github.com/sigstore/k8s-manifest-sigstore - [8] Kubernetes Blog Verifying Container Image Signatures Within CRI Runtimes
https://kubernetes.io/blog/2023/06/29/container-image-signature-verification/