【実務・中級編】 コンテナイメージの署名と検証(Cosign/Notary) – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

コンテナの「中身」だけ見て安心していませんか?サプライチェーン攻撃のリアル

おい、ちょっと手を止めて聞いてくれ。
お前らが日々、Docker HubやGitHub Container Registry(GHCR)から「人気があるから」「公式っぽいから」という理由だけでPullしてくるそのベースイメージ、本当に安全と言い切れるか?

「脆弱性スキャナー(TrivyやClairなど)でCVEのチェックは通してるから大丈夫です」――そんな声が聞こえてきそうだな。甘い。実務の現場で数々のインシデントを踏んできた俺から言わせれば、それはセキュリティ対策のほんの入り口、いや、フェンスのない牧草地で鍵をかけて安心しているようなものだ。

脆弱性スキャナーは「既知のバグ」を見つけることはできるが、「そのイメージが、誰によって、どのコードから、どうやってビルドされたか」の正当性を証明してはくれない。もし、お前らが信頼しているパブリックリポジトリの裏で、攻撃者が権限を奪い取り、コンテナのビルドプロセスに悪意あるバイナリ(マイニングツールやバックドア)をこっそり埋め込んだとしたらどうなる?

スキャナーはそれを「正常なコードのアップデート」と誤認するか、あるいは巧妙に隠蔽されたマルウェアを見逃すかもしれない。そしてお前らは、そのトロイの木馬を本番環境のKubernetesクラスタへ堂々とデプロイしてしまうわけだ。

今回は、このモダンなインフラを揺るがすサプライチェーン攻撃の脅威から身を守るための切り札、「コンテナイメージの署名と検証(Cosign)」について、現場の泥臭い実装ノウハウを含めて徹底的に解説する。

—

攻撃者の手口:パブリックレジストリとイメージすり替えの脅威

攻撃者は、組織の強固なWAFやファイアウォールを直接突破しようとはしない。彼らが好むのは、開発者が無防備に信頼している「サプライチェーンの脆弱な隙間」だ。

よくあるシナリオを一つ教えよう。
1. 攻撃者は、ターゲット企業が利用しているオープンソースのCI/CDパイプラインや、開発者の個人アカウントの認証情報をフィッシングや脆弱性(CIツールの設定ミスなど)をついて窃取する。
2. 攻撃者はレジストリ上の正当なベースイメージ(例: node:18-alpine や社内用の共通イメージ)を、悪意あるコードを仕込んだ同名のイメージにすり替える(Tag Hijacking / Image Poisoning)。
3. 開発者や自動デプロイツール(ArgoCDやGitHub Actionsなど)は、いつも通りそのイメージをPullし、本番環境で起動する。
4. コンテナが起動した瞬間、ホストのカーネル脆弱性を突くエクスプロイトが走るか、あるいは環境変数に保持されているAWSのIAMクレデンシャルが外部のC2サーバーへと送信される。

ここで問題なのは、「レジストリに保存されているイメージのハッシュ値(Digest)やタグが、意図しないものに書き換えられていても、名前とタグだけでは人間には見分けがつかない」という点だ。

この脅威を根絶するために必要なのが、「このイメージは確実にうちの公式CI/CDパイプラインがビルドしたものだ」という暗号学的証明(デジタル署名)と、「デプロイ時にその署名を検証し、偽物なら絶対に起動させない仕組み(Admission Controller等による強制)」である。

—

Sigstore Cosignによるデジタル署名と検証の実装

今回は、CNCF(Cloud Native Computing Foundation)のプロジェクトであり、キーレス署名やOIDC(OpenID Connect)連携もサポートしているデファクトスタンダードツール Cosign を使った実装ハンズオンを紹介する。

従来のGPGキー管理のような面倒な鍵の配付地獄から解放される、現代的なアプローチを見ていこう。

1. 署名用鍵ペアの生成(ローカル管理の例)

まずは、イメージに署名するための秘密鍵と公開鍵を生成する。実務では環境変数にパスワードを設定して非対話型で生成するのが鉄則だ。

# 秘密鍵(cosign.key)と公開鍵(cosign.pub)を生成
# 実行時にパスワードの入力を求められる
cosign generate-key-pair

生成された cosign.key は絶対にGit管理に含めず、セキュアなストレージ(GitHub SecretsやVaultなど)で厳重に保護しろ。cosign.pub は検証を行うすべての環境(Kubernetesノードやデプロイサーバー)に配置する。

2. コンテナイメージへの署名(CI/CDパイプライン内)

次に、Dockerイメージをビルド&プッシュした後、そのイメージダイジェストに対して署名を付与する。ここでのポイントは、「タグ(例: latest)」ではなく「ダイジェスト(例: sha256:abcdef...)」に対して署名することだ。タグは上書きできるが、ダイジェストは不変だからだ。

# 環境変数の定義
IMAGE="ghcr.io/your-org/my-secure-app:v1.0.0"

# イメージのダイジェスト値を取得して厳密に署名を実行
# ※事前に docker push が完了している前提
cosign sign --key cosign.key ${IMAGE}

このコマンドを実行すると、Cosignはイメージと同じレジストリ内に、署名データを含んだ別タグ(sha256-xxx.sig のようなアノテーション)を自動的にプッシュしてくれる。元のイメージ本体のコードを汚染しないスマートな仕組みだ。

3. デプロイ時の検証(PythonによるCI/CDまたは検証スクリプトの例)

「本当に正しい署名がされているか」をデプロイ直前にプログラムやスクリプトで検証する。ここでは、デプロイメントパイプラインやカスタムチェッカーで使えるPythonのサンプルコードを示す。

実務では subprocess モジュール等を用いて cosign verify コマンドを安全に呼び出し、例外処理を確実に行うことが求められる。

import subprocess
import sys

def verify_container_image(image_ref: str, public_key_path: str) -> bool:
    """
    Cosignを使用してコンテナイメージのデジタル署名を検証する関数
    
    :param image_ref: 検証対象のイメージ名(ダイジェスト付きを推奨)
    :param public_key_path: 検証用公開鍵のパス
    :return: 検証成功時は True、失敗時は False
    """
    # cosign verify コマンドの構築
    # --key で公開鍵を指定し、対象のイメージリファレンスを渡す
    cmd = [
        "cosign", "verify",
        "--key", public_key_path,
        image_ref
    ]

    print(f"[*] 署名検証を開始します: {image_ref}")

    try:
        # サブプロセスとしてcosignコマンドを実行
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True
        )
        print("[+] 署名検証に成功しました。安全なイメージです。")
        print(result.stdout)
        return True

    except subprocess.CalledProcessError as e:
        print("[-] 警告: コンテナイメージの署名検証に失敗しました!", file=sys.stderr)
        print(f"[-] エラー詳細:\n{e.stderr}", file=sys.stderr)
        return False

if __name__ == "__main__":
    # 実務での利用例
    TARGET_IMAGE = "ghcr.io/your-org/my-secure-app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    PUB_KEY = "./cosign.pub"

    is_valid = verify_container_image(TARGET_IMAGE, PUB_KEY)
    
    if not is_valid:
        # 検証に失敗した場合はデプロイを即座にアボートする
        print("[-] デプロイメントを中止します。", file=sys.stderr)
        sys.exit(1)
    
    sys.exit(0)

このスクリプトをKubernetesのAdmission Controller(KyvernoやConnaisseurなど)や、GitHub Actionsのデプロイステップの直前に挟み込むことで、「署名のない、あるいは改ざんされたイメージは、絶対にクラスタ内でコンテナとして起動させない」という強固な防御壁が完成する。

—

セキュリティチーフからの実務アドバイス

コンテナの署名と検証を導入するにあたって、現場のエンジニアによくある勘違いと注意点をいくつか伝えておく。

1. 「タグ」ではなく「ダイジェスト」で縛れ
デプロイマニフェスト(KubernetesのDeploymentなど)で image: my-app:latest や image: my-app:v1.0.0 と指定している場合、レジストリ側でそのタグが上書きされるリスクが残る。必ず image: my-app@sha256:xxxx... というダイジェスト指定とセットで署名検証を行う運用を徹底しろ。
2. 鍵のライフサイクル管理を軽視するな
今回紹介したローカルキーペア(秘密鍵・公開鍵)方式はシンプルだが、秘密鍵を紛失したり漏洩させたりしたときのリカバリー手順(鍵のローテーション方針)をあらかじめチーム内でドキュメント化しておけ。可能であれば、GitHub ActionsのOIDCプロバイダと連携した「キーレス署名(Keyless Signing)」の導入も検討すると運用負荷が劇的に下がる。
3. 「セキュリティはプロセスである」
どれだけ強力な署名ツールを入れても、開発者のローカルPCがマルウェアに感染し、そこから不正なコードがビルドされてしまっては元も子もない。SAST(静的解析)、SCA(脆弱性診断)、そして今回のソフトウェア署名(Cosign)をパイプライン全体にシームレスに組み込むことだ。

インフラを守るのは、派手なハッキングテクニックの応酬じゃない。こうした地味で確実な「信頼のチェーン(Chain of Trust)」を一枚一枚、泥臭く積み上げていく作業そのものだ。

さあ、お前の管理するレジストリとデプロイパイプラインを見直してこい。今日もセキュアなシステム構築を頼むぞ。

コメント

タイトルとURLをコピーしました