午前2時14分の緊急呼び出し:イメージの整合性が不可欠な理由
スマートフォンのアラートは非情でした。Kubernetesクラスター内の本番環境ポッドがクラッシュしているという内容です。ログを確認すると、既知のMoneroマイニングプールへの外部接続が見つかりました。45分間調査した結果、冷酷な現実が判明しました。
プライベートレジストリが侵害されたわけではありません。攻撃者は「タグの書き換え(tag-overwrite)」を行ったのです。私たちの node:20-alpine イメージを、悪意のあるバージョンに置き換えていました。見た目は全く同じでしたが、隠されたマイニングツールとリバースシェルが仕込まれていました。CI/CDパイプラインは、タグが一致していたため、何の疑いもなくそのイメージをプルしてしまったのです。
これがソフトウェアサプライチェーン攻撃の現実です. レジストリや単純なタグを信頼することは、いつか負ける賭けをしているようなものです。クラスターで実行されているバイナリが、ビルドシステムで生成されたものと全く同一であることを証明する手段が必要です。ここで、Cosign と Sigstore プロジェクトがゲームチェンジャーとなります。
進化の過程:GPG vs. Notary vs. Cosign
Cosignに移行する前に、いくつかの署名方法を検討しました。DevOpsに数年携わっている方なら、Docker Content Trust (Notary v1) を覚えているかもしれません。志は高かったのですが、スケールさせるのは悪夢でした。現在の状況は以下の通りです:
- GPG署名: パッケージ署名には馴染みがありますが、コンテナには不向きです。「信頼の網(web of trust)」を管理し、何百ものノードに公開鍵を配布するのは、運用上の大惨事を待っているようなものです。
- Docker Content Trust (Notary v1): 署名を保存するためだけに、別途Notaryサーバーと専用のMySQLデータベースが必要でした。構造が脆く、レジストリ間でイメージを移動すると署名が壊れることがよくありました。
- Cosign (Sigstore): これが新しいゴールドスタンダードです。Cosignは署名をOCIアーティファクトとして扱います。署名はイメージと一緒にレジストリ内に保存されます。レジストリがコンテナを保存できるなら、署名も保存できます。追加のデータベースは不要です。
メリット、デメリット、そしてキーレス署名
Cosignへの切り替えは、単にトレンドを追ったわけではありません。エンジニアリングチームにとっての実用的なトレードオフを検討する必要がありました。
メリット
- ポータビリティ: 署名はタグ(例:
sha256-abc.sig)として保存されます。イメージをghcr.ioからdocker.ioに移行しても、署名はシームレスに一緒に移動します。 - キーレス署名: これこそが「アハ体験」の瞬間です。Fulcio(認証局)とRekor(透明性ログ)を使用することで、GitHubやGoogleアカウントなどのOIDCアイデンティティを使用してイメージに署名できます。漏洩を心配する秘密鍵はもう存在しません。
- ネイティブな統合: GitHub Actionsに完璧にフィットし、Kyvernoのようなアドミッションコントローラーと連携して、署名のないイメージをクラスターの入り口でブロックできます。
課題
- 公開性: 公開のSigstoreインスタンスを使用する場合、メールアドレスと署名のタイムスタンプが公開の不変台帳に記録されます。高いセキュリティが求められる企業プロジェクトでは、プライベートなSigstoreスタックをホストする必要があります。
- エコシステムの進化の速さ: プロジェクトの進展が非常に速いです。マイナーバージョン間でもCLIフラグに破壊的変更が見られたことがあるため、Cosignのバージョンを固定(ピン留め)しておくことが必須です。
推奨されるワークフロー
私はハイブリッド戦略を提案します。CI/CDパイプラインではキーレス署名を使用し、シークレット管理の手間を排除します。OIDCが利用できないエアギャップ環境では、ハードウェアセキュリティモジュール(HSM)やAWS KMSのような管理サービスに保存された秘密鍵による鍵ベースの署名を使用します。
最初のレジストリサービスアカウントを設定した際、toolcraft.app/ja/tools/security/password-generator のパスワードジェネレーターを使用しました。これは完全にブラウザ内で動作します。データがマシンから出ることがないため、レジストリの認証情報に必要な高エントロピーな文字列を生成するのに適した選択肢です。
ステップ・バイ・ステップ:最初のイメージ署名
手動での署名プロセスを確認してみましょう。自動化する前に、その仕組みを理解するのに役立ちます。
1. Cosignのインストール
Linuxでは、最新のバイナリを取得します。現時点では v2.4.0 が安定版です:
curl -L -o cosign https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64
chmod +x cosign
sudo mv cosign /usr/local/bin/cosign
2. 鍵ペアの生成
キーレス署名の準備ができていない場合は、まずローカルの鍵ペアから始めます。秘密鍵ファイルを暗号化するためのパスワードを求められます。
cosign generate-key-pair
これにより cosign.key と cosign.pub が生成されます。秘密鍵は安全に保管してください。紛失すると、アップデートに署名できなくなります。
3. イメージへの署名
イメージがプッシュされたら(例:my-registry.com/app:v1.0)、署名を行います:
cosign sign --key cosign.key my-registry.com/app:v1.0
Cosignはイメージのダイジェストを計算し、.sig オブジェクトをレジストリにプッシュします。これには2秒ほどしかかかりません。
4. 検証
検証こそがセキュリティの要です。公開鍵を持っている人なら誰でも以下を実行できます:
cosign verify --key cosign.pub my-registry.com/app:v1.0
イメージが1バイトでも変更されていれば、検証は失敗し、ゼロ以外の終了コードで終了します。
GitHub Actionsによる自動化(キーレス)
モダンなCIにはキーレス署名が最適です。これはFulcioによって発行される、GitHubワークフローのアイデンティティに紐づいた短命な証明書(有効期限10分)を使用します。
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDCに必須
packages: write
steps:
- uses: actions/checkout@v4
- uses: sigstore/[email protected]
- name: ビルドとプッシュ
run: |
docker build -t ghcr.io/${{ github.repository }}:latest .
docker push ghcr.io/${{ github.repository }}:latest
- name: イメージへの署名
run: cosign sign --yes ghcr.io/${{ github.repository }}:latest
シークレットも、GPGキーも、煩わしさもありません。このイメージを検証するには、鍵の代わりにアイデンティティを確認します:
cosign verify ghcr.io/my-org/my-repo:latest \
--certificate-identity-regexp https://github.com/my-org/my-repo/.github/workflows/ \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
最後に:強制適用
イメージへの署名は戦いの半分に過ぎません。それを「強制」する必要があります。Kubernetesクラスターで Kyverno のようなアドミッションコントローラーを使用してください。特定のGitHubアイデンティティによって署名されていないイメージを持つポッドを拒否するポリシーを設定できます。
もし、あの午前2時の事件の時にこれがあれば、悪意のあるイメージは即座にブロックされていたでしょう。Kubernetesは署名のないコードのプルを拒否し、私は眠り続けることができたはずです。セキュリティとは単一のツールのことではなく、コミットから始まりクラスターで終わる「信頼の鎖」を構築することなのです。

