6ヶ月前、私たちのプラットフォームチームはリリースパイプラインにおける重大な盲点を解消する取り組みを始めました。本番クラスタで稼働しているイメージが、実際にGitHub ActionsのCIパイプラインからビルドされたものであることを自動検証する仕組みが存在しなかったのです。
もし誰かが追跡されていないイメージをAWS ECRレジストリに直接プッシュした場合、Kubernetesは何の疑問も持たずにそれをプルして実行してしまいます。45のマイクロサービス、週に約250回のデプロイが行われる環境でこのセキュリティギャップを埋めるため、私たちはSigstoreプロジェクトのCosignを導入しました。本記事では、コンテナ署名の実際の仕組み、優れた点、遭遇したトラブル、そしてアドミッションポリシーとの連携方法について解説します。
コンテナイメージ署名アプローチの比較
コンテナの出所(Provenance)を保証するアプローチは、主にDocker Content Trust(Notary v1)、Notation(Notary v2)、Cosign(Sigstore)の3つに集約されます。それぞれ署名の管理、メタデータの保存場所、鍵管理の方法が異なります。
| アプローチ | 署名の保存先 | 鍵管理 | 導入コスト・障壁 |
|---|---|---|---|
| Docker Content Trust (Notary v1) | 独立したNotaryサーバーおよびMySQL/PostgresメタデータDB | 長期有効なクライアントキー(TUF root/targetsキー) | 高(独立したNotaryサーバーのホスティングとスケーリングが必要) |
| Notation (Notary v2) | OCIレジストリ(OCI Artifactのreferrerとして保存) | X.509 PKI証明書またはCloud KMS | 中(エンタープライズ向けの正式なPKI向けに特化して設計) |
| Cosign (Sigstore) | OCIレジストリ(同一場所の署名マニフェストまたはreferrer) | Keyless(OIDC + Fulcio + Rekor)、静的キーペア、またはKMS | 低(追加のデータベース不要で標準的なOCIレジストリ上で動作) |
Docker Content Trustは、開発スピードの速いチームにとっては運用が脆すぎることが判明しました。リレーショナルデータベースを備えた専用のNotaryサーバーをデプロイする必要があり、Notaryインスタンスでネットワークの瞬断やキャッシュ無効化のバグが発生すると、本番環境へのデプロイ全体が失敗してしまう問題がありました。
Cosignは、既存のコンテナレジストリをそのままストレージバックエンドとして使用することでこの問題を解決します。myregistry.com/app:v1.0.0に署名すると、Cosignはそのイメージのすぐ横に署名マニフェストを直接アップロードします。保守すべき外部データベースも、同期すべき追加の状態(ステート)も存在しません。
本番運用6ヶ月で分かったメリットとデメリット
1,200以上のPodが稼働する環境にCosignを導入したことで、このツールの優れた点と運用上の課題が明確になりました。
メリット
- OIDCによるKeyless(鍵なし)署名: GitHub Actionsから発行される有効期間の短いOIDCトークンをFulcio(Sigstoreの認証局)およびRekor(透明性ログ)と組み合わせることで、長期有効な署名鍵の管理を完全に撤廃できました。開発者が秘密鍵を生成、ローテーション、または保存する必要は一切ありません。
- 幅広いレジストリサポート: Cosignは、カスタムプラグインや専用のAPIアダプターを必要とせず、AWS ECR、Google Artifact Registry、Harborなどで導入直後からシームレスに動作しました。
- 高速なアドミッションチェック: Kyvernoを使用してKubernetesのアドミッション制御レイヤーで署名を検証する際の遅延は、PodのアドミッションWebhookに対して45ms未満にとどまりました。
- アテステーション(Attestation)とSBOMへの対応: Cosignが扱えるのは生のイメージダイジェストだけではありません。同一のCLIワークフローを使って、CycloneDX形式のソフトウェア部品表(SBOM)やSLSAビルドProvenanceファイルをコンテナイメージに署名・添付しています。
デメリット・課題
- パブリックRekorでのメタデータ漏洩: パブリックなSigstoreは、ビルドクレームやコミットメールアドレスを公開台帳に記録します。クローズドソースのプロプライエタリソフトウェアを運用しており、リポジトリURLやエンジニアのメールアドレスを公開できない場合は、Sigstoreスタックをセルフホストするか、プライベートなCloud KMSキーを利用する必要があります。
- レジストリのライフサイクルポリシーとの競合: Cosignは
sha256-[digest].sigという形式のタグアーティファクトを作成します。導入から2週間後、ECRのライフサイクルポリシーによってタグなしのベースイメージが削除され、複数の署名が孤立してステージング環境のデプロイが停止する事態が発生しました。そのため、*.sigや*.attタグを明示的に保護するようにレジストリの保持ルールを書き直す必要がありました。 - エアギャップ環境における運用オーバーヘッド: 隔離されたVPCやエアギャップ(閉域網)データセンターでKeyless Sigstoreを実行するには、Fulcio、Rekor、Trillian、およびDexなどのOIDCプロバイダーを自前でホストする必要があります。この構成は相応の運用保守コストを伴います。
推奨するKubernetesアーキテクチャ
多くのクラウドエンジニアリングチームにとって、セキュリティと運用のシンプルさのバランスが最も優れているのはハイブリッド構成です。
- CI/CDパイプライン: クラウドCI(GitHub Actionsなど)でのOIDC Keyless認証、またはCloud KMS(AWS KMS、Google Cloud KMS、HashiCorp Vault Transit)を使用して、ビルドジョブ中にイメージに署名します。
- クラスタでの強制適用: Validating Admission WebhookとしてKyvernoまたはGatekeeperをデプロイし、Podのスケジューリングが開始される前に、検証済みの暗号署名を持たないコンテナをブロックします。
ステップ別実装ガイド
以下は、鍵の生成、イミュータブル(不変)なダイジェストによるイメージの署名、そしてKubernetes内での検証の強制適用を行う完全な手順です。
ステップ1: Cosign CLIのインストール
ビルドラナーまたはワークステーション用の最新バイナリを取得します。
# Cosignバイナリのダウンロードとインストール (Linux amd64)
LATEST_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -LO "https://github.com/sigstore/cosign/releases/download/${LATEST_VERSION}/cosign-linux-amd64"
sudo install -o root -g root -m 0755 cosign-linux-amd64 /usr/local/bin/cosign
# バイナリの動作確認
cosign version
ステップ2: 暗号鍵ペアの生成
パブリックなKeyless署名を使用しない場合は、パスワードで保護されたキーペアを生成します。
# cosign.key(秘密鍵)とcosign.pub(公開鍵)を生成
cosign generate-key-pair
# プロンプトが表示されたら安全なパスフレーズを入力
cosign.keyをCIのシークレットマネージャー(HashiCorp VaultやAWS Secrets Managerなど)に保存します。cosign.pubはKubernetesポリシーに埋め込むため手元に控えておきます。
ステップ3: コンテナイメージの署名
イメージへの署名は、latestやv1.2.0のような変更可能なタグではなく、必ずイミュータブルなSHA256ダイジェストを使用して行います。タグは付け替えることができますが、ダイジェストは変更できません。
# CIパイプラインで鍵のパスワードを指定
export COSIGN_PASSWORD="YourProductionSecretPassword"
# 正確なイメージダイジェストを定義
IMAGE_DIGEST="registry.example.com/production/auth-service@sha256:7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"
# イメージに署名
cosign sign --key cosign.key "${IMAGE_DIGEST}"
Cosignは署名ペイロードを計算し、新しいマニフェストレイヤーとしてリポジトリに直接アップロードします。
ステップ4: イメージの手動検証
公開鍵を使用してターミナルから署名をテストします。
cosign verify --key cosign.pub "${IMAGE_DIGEST}"
ペイロードが真正でありダイジェストが一致した場合、Cosignは検証クレームをJSON形式で出力します。イメージが改ざんされているか、不明な鍵で署名されている場合、Cosignはゼロ以外の終了コードを返します。
ステップ5: Kyvernoを用いたアドミッション時の検証強制
CIでの手動チェックだけでは、エンジニアがkubectlを使って未検証のコンテナを直接実行するのを防ぐことはできません。私たちはKyvernoのClusterPolicyを使用して、クラスタの境界でイメージ検証を強制しています。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-image-signature
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-trusted-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/production/*"
key: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7p9r7K9eJ8h9qK3vQ1Z5w8xL
3oR7s+9p4qL8k7m6w5b0x2j3z5v8t1y4u7i0o3p6a9s2d5f8g1h4j7k0l3==
-----END PUBLIC KEY-----
ポリシーをクラスタに適用します。
kubectl apply -f verify-policy.yaml
署名されていないイメージでPodを作成し、強制適用の動作をテストします。
kubectl run test-pod --image=registry.example.com/production/auth-service:untrusted
Kubernetes APIサーバーはリクエストを即座に拒否します。
Error from server: admission webhook "validate.kyverno.svc" denied the request:
ClusterPolicy enforce-image-signature failed: image signature verification failed for registry.example.com/production/auth-service:untrusted
次のステップ
署名の強制適用を導入する際、開発チームの作業を停滞させる必要はありません。まずはvalidationFailureAction: Auditを設定してアドミッションポリシーをデプロイすることから始めましょう。これにより、進行中のデプロイをブロックすることなく、モニタリングダッシュボード上で未署名のイメージを検知できます。CIパイプラインで本番ビルドの100%に対して確実に署名が行われるようになったら、ポリシーをEnforceモードに切り替えます。

