本番環境における出所不明なDockerイメージの脅威
最近10件以上のシステムでセキュリティ監査を実施した際、よく見かける典型的なパラドックスに遭遇しました。エンジニアチームは毎週OSのパッチを適用し、ファイアウォールの各ポートを厳格に制限しています。しかし一方で、サーバーにプルされるコンテナイメージはほぼ野放し状態であり、そのイメージファイルが本当に改ざんされていない正当なものかを検証している人は誰もいませんでした。
Dockerタグはmutable(上書き可能)な性質を持っています。この脆弱性は、実際の運用において多くの深刻なリスクをもたらします。
- レジストリ認証情報の漏洩:攻撃者がDocker Hubや社内Harborのトークンを奪取し、チームがまさにデプロイ準備を進めている
:v1.0.0タグに暗号資産マイニングマルウェア(XMRigなど)を上書きプッシュする。 - 中間者攻撃(MitM):社内ネットワークやプロキシ経由のイメージダウンロードトラフィックが傍受・改ざんされ、転送途中でマニフェストが差し替えられる。
- 安全性の不透明なベースイメージの誤プル:開発者がバックドアが仕込まれたイメージや、パッチ未適用の深刻なCVEを含むイメージを誤って使用してしまう。
本番サーバーは、自社の正規パイプラインでビルドされたイメージと、第三者によって途中で混入された悪意あるビルドをどのように見分ければよいのでしょうか?これこそが「ソフトウェアサプライチェーンセキュリティ(Software Supply Chain Security)」における極めて重要な課題です。
Sigstore Cosign:軽量かつ実用的な電子署名ソリューション
従来は、Docker Content Trust(Notary v1)が最も一般的なソリューションでした。しかしNotaryは専用サーバーの構築を必要とし、TUFメタデータの管理が複雑でメンテナンスコストも非常に高額でした。そのため、多くのDevOpsチームはその煩雑さに耐えかねて導入を断念していました。
Linux Foundationが支援するSigstoreプロジェクトの一部であるCosignは、この課題を極めてシンプルに解決します。Cosignは新たなデータベースを構築する必要がありません。既存のコンテナレジストリ(Docker Hub、GHCR、AWS ECR、Harborなど)自体を署名の格納先として活用します。署名時、Cosignは署名ペイロードを含むOCIアーティファクトを生成してレジストリへ直接プッシュし、元イメージのsha256ダイジェストと強固に紐付けます。
現在、主に2つの署名方式があります。
- キーペア署名(Key-based signing):お馴染みの秘密鍵/公開鍵を生成する方式です。秘密鍵はCIランナーのSecretに保持して署名に用い、公開鍵は検証対象の本番サーバーに配置します。中小規模のチームにとって非常に導入しやすい構成です。
- キーレス署名(Keyless signing):Fulcioを介したOpenID Connect(OIDC)を活用して短命(数分間有効)な証明書を発行し、Rekor透明性ログに記録する方式です。長期間有効な秘密鍵の漏洩リスクを完全に排除できます。
実践:ステップバイステップで進めるDockerイメージの署名と検証
ステップ1:LinuxへのCosignのインストール
Sigstore公式からバイナリを直接ダウンロードします:
# cosign v2.x バイナリのダウンロード
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
# リネームして実行権限を付与
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign
# インストールの確認
cosign version
ステップ2:Cosignキーペアの生成
キーペア生成コマンドを実行します:
cosign generate-key-pair
秘密鍵を保護するためのパスフレーズの入力が求められます。CI/CDによる自動実行時は、環境変数COSIGN_PASSWORDを事前に設定しておくことで、プロンプト待ちによるスクリプト停止を防ぐことができます。
コマンドの実行が完了すると、以下の2つのファイルが生成されます:
cosign.key:署名に使用する秘密鍵。GitHub SecretsやVaultなどでのみ厳重に管理し、決して外部に漏洩させてはいけません。cosign.pub:検証に使用する公開鍵。このファイルは本番ノードへ安全に広く配布できます。
ステップ3:イメージのビルドとレジストリへのプッシュ
重要な注意点として、Cosignはイメージがリモートレジストリに存在している状態でのみ署名可能です。ログインしてテストイメージをプッシュしましょう:
docker login
# タグ付けしてテストイメージをプッシュ
docker tag alpine:3.19 your-dockerhub-user/secure-app:1.0.0
docker push your-dockerhub-user/secure-app:1.0.0
ステップ4:イメージへのデジタル署名
秘密鍵を指定してcosign signコマンドを実行します:
cosign sign --key cosign.key your-dockerhub-user/secure-app:1.0.0
ステップ2で設定したパスフレーズを入力します。直後にCosignは、署名を保持するためのプレフィックスsha256-<digest>.sigを持つ新しいタグをレジストリへ自動的にプッシュします。
ステップ5:本番環境での署名検証
本番サーバー(cosign.pubファイルのみを保持する環境)から、検証コマンドを実行します:
cosign verify --key cosign.pub your-dockerhub-user/secure-app:1.0.0
署名が一致し、イメージが改ざんされていなければ、Cosignはダイジェストを含むJSONペイロードを返します:
[
{
"critical": {
"identity": { "docker-reference": "index.docker.io/your-dockerhub-user/secure-app" },
"image": { "docker-manifest-digest": "sha256:d4e3..." },
"type": "cosign container image signature"
}
}
]
攻撃シナリオをシミュレートしてみましょう。別のイメージをビルドし、再署名を行わずに1.0.0タグへ上書きプッシュします。すると、検証コマンドは即座にエラーコードを返します:
Error: no matching signatures:
failed to verify signature
ステップ6:デプロイスクリプトへの統合
不正なコンテナが誤って実行されるのを防ぐため、終了ステータスコードをチェックするスクリプトでラップします:
#!/usr/bin/env bash
set -euo pipefail
IMAGE="your-dockerhub-user/secure-app:1.0.0"
PUBLIC_KEY="/opt/security/cosign.pub"
echo "[*] 完全性を検証中: ${IMAGE}"
if cosign verify --key "${PUBLIC_KEY}" "${IMAGE}" > /dev/null 2>&1; then
echo "[+] 署名は有効です。コンテナを起動しています..."
docker run -d --name production-app "${IMAGE}"
else
echo "[!] エラー: イメージが署名されていないか、改ざんされています。デプロイを中止します!" >&2
exit 1
fi
Kubernetesを運用している場合は、シェルスクリプトの代わりにKyvernoやOPA GatekeeperなどのAdmission Controllerを採用できます。これにより、正当な署名検証を通過していないイメージをプルしようとするPodを自動で拒絶できます。
おわりに
サーバー自体を守ることは必要条件にすぎません。サーバー上にデプロイされるものを厳密に制御できてこそ十分条件となります。Cosignのセットアップに必要な時間はわずか15分程度ですが、本番環境で悪意あるコンテナを誤って稼働させてしまうリスクを根底から排除してくれます。

