Certified Kubernetes Security Specialist (CKS) #15 イメージ署名: cosign、SBOM

#14 イメージスキャン: Trivy、Kubesec、KubeLinter では、イメージの中にどんな脆弱性が入っているかをスキャンで覗き込みました。ところがスキャンは「このイメージは安全か」を見る作業であり、その前にもっと根本的な問いがあります。「このイメージは本当に私たちが作ったあのイメージなのか」 です。レジストリから取ってきた myapp:1.0 が私たちの CI がビルドしたものなのか、それとも攻撃者が同じタグで押し込んだ別のイメージなのかは、タグだけでは分かりません。タグはいつでも上書きできるからです。

この信頼の空白を埋めるのが イメージ署名 です。ビルドの主体がイメージに署名を残し、デプロイ時点でその署名を検証すれば、出所と完全性をともに保証できます。この記事では、sigstore の cosign で署名と検証を扱い、SBOM (Software Bill of Materials) でイメージが何で構成されているかを文書化する 2 つの軸を整理します。

なぜ署名か: 信頼の出発点 #

コンテナイメージはレジストリにアップロードされた瞬間から、誰でも受け取れる公開資産になります。問題は タグが識別子であって信頼の証拠ではない点 です。registry.example.com/myapp:1.0 という名前は、レジストリ内の 1 つのマニフェストを指すポインターにすぎず、書き込み権限を持つ誰かが同じタグを別のイメージで上書きすれば、名前はそのままで中身だけが変わります。これがサプライチェーン攻撃の典型的な入り口です。

イメージには変わらない識別子が 1 つあります。ダイジェスト (digest) です。sha256: で始まるこのハッシュはイメージ内容全体から計算されるので、内容が 1 バイトでも異なればダイジェストが変わります。そのため完全性の面では、タグの代わりにダイジェストでイメージを固定するのが第一歩です。

# タグの代わりにダイジェストでイメージを固定する
spec:
  containers:
    - name: app
      image: registry.example.com/myapp@sha256:3a1b...e9f0

ところがダイジェストは 完全性 を保証するだけで 出所 を証明することはできません。「このイメージはビルド以降に変わっていない」は分かりますが、「このイメージを信頼できる主体が作った」は別の問いです。出所まで証明するにはビルド主体の署名が必要です。ここで cosign が登場します。

cosign: sigstore の署名ツール #

sigstore はソフトウェアサプライチェーンに署名と検証を手軽に導入するためのオープンソースプロジェクトで、cosign はその中でコンテナイメージに署名し検証するコマンドラインツールです。cosign の核心は、署名を別途のインフラなしに イメージと同じレジストリに保存する 点です。署名は元のイメージダイジェストに紐づいた別アーティファクトとしてアップロードされます。

cosign がサポートする署名方式は大きく 2 つです。

方式鍵管理特徴
鍵ベース (key pair)秘密鍵・公開鍵を直接保管シンプル。鍵漏洩・ローテーションを自分で責任
keyless (OIDC)鍵の保管なしOIDC 身元で短期証明書を発行。CI に適する

試験では 2 つの方式のうち 鍵ベース署名・検証 が身につけておくべき基本であり、keyless は概念として理解しておけば十分です。

鍵ペアの生成 #

まず署名に使う鍵ペアを作ります。パスワードを入力すると、そのパスワードで保護された秘密鍵ファイルと公開鍵ファイルが生成されます。

# cosign.key (秘密鍵)、cosign.pub (公開鍵) が生成される
cosign generate-key-pair
  • cosign.key は署名に使う 秘密鍵 です。決して外部に露出してはいけません。
  • cosign.pub は検証に使う 公開鍵 です。検証主体に配布しても安全です。

運用環境では、秘密鍵をファイルで置くより KMS (AWS KMS、GCP KMS など) や Kubernetes Secret に保管する方式を使います。cosign は cosign generate-key-pair k8s://<namespace>/<secret> の形で鍵をそのまま Secret に入れることもサポートします。

イメージに署名 #

生成した秘密鍵でイメージに署名します。対象イメージは可能なら、タグではなく ダイジェストで指定する のが安全です。タグで署名すると cosign がその時点のダイジェストを解決して署名しますが、明示的にダイジェストを与えると意図が明確になります。

# 秘密鍵でイメージに署名する
cosign sign --key cosign.key registry.example.com/myapp:1.0

署名が終わると、レジストリには元のイメージの隣に sha256-....sig という名前の署名アーティファクトが一緒に保存されます。署名自体がレジストリに保存されるので、別途の署名ストアを運用する必要はありません。

署名の検証 #

デプロイ側では公開鍵で署名を検証します。検証が成功すれば、そのイメージがその秘密鍵の保有者によって署名され、以降変更されていないことを確認できます。

# 公開鍵でイメージ署名を検証する
cosign verify --key cosign.pub registry.example.com/myapp:1.0

検証に成功すると、cosign は署名のペイロードを標準出力に表示します。検証に失敗した場合、つまり署名がない、別の鍵で署名されている、イメージが改ざんされている場合は、0 以外の終了コードとともにエラーを出します。この終了コードが、あとで扱う自動化の出入り口になります。

# 終了コードで検証の成否を判断する
cosign verify --key cosign.pub registry.example.com/myapp:1.0 \
  && echo "署名検証 通過" \
  || echo "署名検証 失敗: デプロイ遮断"

keyless 署名 (OIDC) を一行で #

鍵ベース方式は、秘密鍵を安全に保管し定期的にローテーションしなければならない負担があります。keyless 署名 はこの負担をなくします。署名時点で OIDC 身元 (例: GitHub Actions のワークフロー身元、ユーザーの Google・GitHub アカウント) で認証すると、sigstore の認証局 (Fulcio) がその身元に紐づく 短期証明書 を発行し、署名記録は透明性ログ (Rekor) に残ります。秘密鍵を保管しないので、CI パイプラインで特に適しています。

# OIDC 身元で署名する (ブラウザまたは CI トークンで認証)
cosign sign registry.example.com/myapp:1.0
# keyless 検証はどの身元・発行者を信頼するかを明示する
cosign verify \
  --certificate-identity "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  registry.example.com/myapp:1.0

keyless 検証では、公開鍵の代わりに 誰が (certificate-identity) どこで (certificate-oidc-issuer) 署名したか を信頼条件として指定します。こうすれば「私たちの main ブランチのビルドワークフローが署名したイメージだけ通過」というポリシーを、鍵管理なしで立てられます。

SBOM: イメージを構成する部品リスト #

署名が「このイメージを誰が作ったか」を証明するなら、SBOM (Software Bill of Materials) は「このイメージが何で構成されているか」を文書化します。SBOM は、イメージに入った OS パッケージ、言語ライブラリ、バージョン、ライセンスなど 構成要素の全リスト です。製造業の部品明細書 (BOM) をソフトウェアにそのまま移した概念です。

SBOM が重要な理由は 脆弱性追跡 と直結するからです。新しい致命的脆弱性 (例: 過去の Log4Shell) が公開されると、まず答えるべき問いは「私たちのイメージのうち、そのライブラリを使うものは何か」です。SBOM が事前に作られていれば、この問いに即座に答えられます。SBOM がなければ、すべてのイメージを再スキャンして探さなければなりません。

SBOM 形式: SPDX と CycloneDX #

SBOM は人が読む自由文書ではなく 標準形式 に従います。ツールが生成し別のツールが読み込むには、形式が決まっている必要があるからです。代表的な 2 つの標準があります。

形式主管特徴
SPDXLinux Foundationライセンスコンプライアンス中心から出発。ISO 標準
CycloneDXOWASPセキュリティ・脆弱性追跡中心。軽量

どちらも広く使われ、ツールのほとんどが両形式を出力します。試験では「どの形式で生成せよ」というオプションを正確に指定するのがポイントです。

syft で SBOM を生成 #

syft はイメージやファイルシステムから構成要素を抽出して SBOM を作るツールです。出力形式をオプションで選んで指定します。

# イメージから SBOM を生成する (デフォルト出力)
syft registry.example.com/myapp:1.0
# SPDX (JSON) 形式でファイルに保存する
syft registry.example.com/myapp:1.0 -o spdx-json=sbom.spdx.json
# CycloneDX (JSON) 形式でファイルに保存する
syft registry.example.com/myapp:1.0 -o cyclonedx-json=sbom.cdx.json

生成した SBOM はそれ自体が終わりではなく、次の段階の入力になります。脆弱性スキャナー grype は、イメージを直接スキャンする代わりに SBOM を入力として受け取り、脆弱性をマッチングできます。

# SBOM を入力に脆弱性をマッチングする
grype sbom:sbom.spdx.json

#14 で扱った Trivy も SBOM 生成と SBOM ベーススキャンの両方をサポートします。流れは同じです。構成要素リストを一度作っておき、新しい脆弱性が出るたびにそのリストに照合する のです。

SBOM と署名を一緒に: 添付と attestation #

SBOM はイメージと一緒に動いてこそ意味があります。cosign は SBOM をイメージに 添付 (attach) するか、出所を証明する attestation (証明) として署名して付けることをサポートします。attestation は「このイメージに対するこの SBOM が本物だ」を署名で保証する方式です。

# SBOM を attestation にしてイメージに署名・添付する
cosign attest --key cosign.key \
  --predicate sbom.spdx.json \
  --type spdxjson \
  registry.example.com/myapp:1.0
# 添付された SBOM attestation を検証する
cosign verify-attestation --key cosign.pub \
  --type spdxjson \
  registry.example.com/myapp:1.0

こうすると、イメージの出所 (署名) と構成 (SBOM) がともに検証可能な形でレジストリにまとめて保存されます。サプライチェーンセキュリティの目標がまさにこの状態です。

admission で署名を強制する #

ここまでのコマンドは、人が手で検証する流れでした。しかし実際のクラスターで必要なのは 未署名イメージがそもそもデプロイされないように止めること です。そのためにはクラスターが Pod を受け入れる関所、つまり admission control で署名を検証しなければなりません。

流れはこうです。kubectl が Pod 作成を要求すると API server が admission webhook にその要求を渡し、webhook がイメージ署名を cosign 方式で検証したうえで、通過または拒否を決めます。署名がない、または信頼しない鍵で署名されたイメージを使う Pod は、作成段階で止められます。

kubectl apply
API server ──→ admission webhook (署名検証)
                   │  通過: Pod 作成許可
                   └  失敗: Pod 作成拒否

この検証をポリシーで表現するツールが、まさに次の記事のテーマである OPA/GatekeeperKyverno です。特に Kyverno には verifyImages ルールがあり、「このレジストリのイメージはこの公開鍵で署名されていなければならない」というポリシーを宣言的に書けます。sigstore 陣営の policy-controller も同じ役割をする専用 admission controller です。

# Kyverno: 指定レジストリのイメージに cosign 署名を要求するポリシーの骨子
spec:
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      ...cosign.pub の内容...
                      -----END PUBLIC KEY-----

ポリシーの細かい文法と OPA/Gatekeeper との比較は #16 Admission control: OPA/Gatekeeper、Kyverno で詳しく扱います。この記事では、署名・SBOM という材料を作り、その材料を admission が強制する というつながりを覚えておけば十分です。

試験ポイント #

CKS 試験の Supply Chain Security ドメインでも、イメージ署名と SBOM は次を手に覚えておくのが核心です。

  • 鍵ペア生成cosign generate-key-paircosign.keycosign.pub を作ります。両者の役割の区別を取り違えないようにします。
  • 署名cosign sign --key cosign.key <イメージ> です。可能ならダイジェストで対象を固定します。
  • 検証cosign verify --key cosign.pub <イメージ> です。検証失敗時に 0 以外の終了コードが出る点を押さえておきます。
  • keyless 概念。秘密鍵なしで OIDC 身元から短期証明書を受け取って署名・検証し、検証では --certificate-identity--certificate-oidc-issuer で信頼条件を指定します。
  • SBOM 生成syft <イメージ> -o spdx-json または -o cyclonedx-json で形式を明示します。SPDX と CycloneDX の違いを一行で説明できる必要があります。
  • SBOM の目的。構成要素リストを事前に作り、新しい脆弱性が出たときに影響イメージを即座に探す追跡手段です。
  • ダイジェスト固定。タグは改ざん可能なので、完全性が重要なところは @sha256: ダイジェストでイメージを指定します。

試験では「このイメージに与えられた鍵で署名せよ」「この公開鍵で署名を検証し結果をファイルに残せ」「このイメージの SBOM を SPDX 形式で生成せよ」といった作業が定番です。コマンド一行を正確なオプションとともに打てれば、速く点数になります。

まとめ #

この記事で押さえたこと:

  • タグは信頼の証拠ではありません。完全性はダイジェストで固定し、出所は署名で証明します。
  • cosign。sigstore の署名ツールです。generate-key-pair で鍵を作り、sign で署名し、verify で検証します。署名はイメージが住むレジストリに一緒に保存されます。
  • keyless (OIDC)。秘密鍵の保管なしで身元ベースの短期証明書で署名します。CI パイプラインに適します。
  • SBOM。イメージの構成要素リストです。syft で SPDX・CycloneDX 形式に生成し、新しい脆弱性が出たときに影響範囲を即座に追跡する根拠になります。
  • admission 強制。手検証を超えて、Kyverno・OPA/Gatekeeper・policy-controller で未署名イメージのデプロイをクラスターの入り口で止めます。

署名と SBOM はサプライチェーンセキュリティの 材料 です。この材料をクラスターが自動で強制するようにするのが、最後の一歩です。

次へ: Admission control #

この記事の最後で先送りした問い、「では未署名イメージをどうやってクラスターが自動で拒否するようにするのか」を、次の記事で正面から扱います。

#16 Admission control: OPA/Gatekeeper、Kyverno では、admission webhook が動作する原理、OPA/Gatekeeper の Rego ポリシーと ConstraintTemplate・Constraint の構造、Kyverno の宣言的ポリシーと verifyImages ルール、そして「特定のレジストリ・署名を要求せよ」「privileged Pod を拒否せよ」といった試験定番のポリシーを直接書いてみながら整理します。

X