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

コンテナの「中身」をどれだけ洗っても、サプライチェーンの毒は消せない

インフラストラクチャのモダナイゼーションが進み、私たちのワークロードの大部分はKubernetesやコンテナベースの環境へと移行した。脆弱性スキャナーをCI/CDパイプラインに組み込み、ベースイメージのCVEを日々監視し、パッチを当て続ける――。多くのセキュリティチームが、この終わりのない「モグラ叩き」に疲弊している。

だが、ここで冷徹な事実を突きつけよう。
あなたがどれほど厳格にコンテナ内のOSパッケージをスキャンし、脆弱性をゼロに近づけたとしても、そのイメージ自体がビルドパイプラインの途中で悪意ある攻撃者にすり替えられていたら、防御の層は何の意味も持たない。

攻撃者が好むのは、脆弱なアプリケーションコードを直接ハッキングすることではない。彼らが狙うのは、開発者が信頼しきっている「レジストリ」や「ビルドプロセス」そのものだ。信頼の根拠(Root of Trust)が汚染されていれば、どんなに堅牢なKubernetesのSecurity Contextも、要塞化されたLinuxカーネルも、無力な飾りと化す。

本稿では、このサプライチェーンの根幹を揺るがすリスクに対し、コンテナイメージの暗号学的署名(Cosign/Notary)を用いて「信頼の連鎖」を強制するアーキテクチャの設計と、現場で迷わない実装手法を、最高峰の防衛者の視点から解説する。

—

攻撃者の視点:なぜサプライチェーン攻撃が選ばれるのか

現代のソフトウェアデリバリーは、数多くの外部依存関係(Base Image、npm/Go modules、CI/CDアクション)の上に成り立っている。攻撃者は、アプリケーションのエンドポイントを直接叩くよりも、ビルドパイプラインの緩みをつく方が圧倒的に費用対効果が高いことを知っている。

典型的なシナリオを見てみよう。

1. レジストリ認証情報の窃取: 開発者のマシンの平文設定や、権限過剰なCI/CDのサービスアカウントからトークンが漏洩する。
2. 悪意あるイメージのプッシュ: 正規のタグ(例: myapp:latest)に、バックドア(リバースシェルやマイニングスクール、クレデンシャルスキマー)を仕込んだイメージを上書きプッシュする。
3. 自動デプロイ: GitOpsや自動CDツールが新着タグを検知し、検証なしで本番クラスタへデプロイする。

ここで問いたい。あなたのKubernetesクラスタは、「レジストリから降ろしてきたから安全だ」という性善説に依存していないか?
暗号学的な検証がない環境では、タグやダイジェスト(SHA256)は単なる「名前」に過ぎず、その正当性を証明するものではない。ここに「デジタル署名」を導入する必然性がある。

—

アーキテクチャ設計:Cosignによる鍵管理と署名の全体像

コンテナイメージの署名において、現在最も実用的でエコシステムが成熟しているのがSigstoreプロジェクトの Cosign である。Notary v2の標準化も進んでいるが、現場のスピード感と鍵管理の柔軟性を考慮すると、Cosignはそのデファクトスタンダードと言って過言ではない。

Cosignのアーキテクチャにおいて最も重要なのは、「誰が(Identity)」署名したのかを、公開鍵暗号とOIDC(OpenID Connect)プロバイダを通じて保証する点だ。

鍵レス署名(Keyless Signing) vs 秘密鍵管理

  • 鍵レス署名: GitHub ActionsなどのCI/CD環境のOIDCトークンを利用し、Sigstoreの公開証明書局(Fulcio)と透明性ログ(Rekor)を組み合わせる方式。秘密鍵のローテーション管理から解放されるため、現代のクラウドネイティブ環境では強く推奨される。
  • 従来型秘密鍵: 組織内で生成したECDSA/RSAの秘密鍵をKMS(AWS KMS, HashiCorp Vault等)に保管し、それを使って署名する方式。エアギャップ環境や厳格なコンプライアンス要件がある場合に選択する。

本稿では、実務で最も堅牢かつ導入が進んでいる「KMS連携または鍵レスによるCI/CD署名」を前提に、検証の仕組みを組み立てる。

—

実装:CI/CDパイプラインへの署名組込みとKubernetesでの強制

ここからは、理論を具体的なコードと設定に落とし込む。以下のフローは、セキュアなサプライチェーンのミニマムかつ完全な実装例だ。

1. CI/CD環境でのイメージ署名(GitHub Actionsの例)

ビルドしたイメージをレジストリ(例: GHCRやAmazon ECR)にプッシュした後、そのダイジェストに対してCosignで署名を行う。ここでは鍵レス署名(OIDC利用)の例を示す。

name: Build and Sign Container Image

on:
  push:
  branches: [ "main" ]

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: "critical" # 鍵レス署名に必要なOIDCトークン発行権限

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        id: build-and-push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/your-org/your-app:latest

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3.5.0

      - name: Sign the container image using Keyless (OIDC)
        env:
          # Cosignが実験的機能を有効にするための環境変数(v2.0以降はデフォルトで有効な機能も多いが明示)
          COSIGN_EXPERIMENTAL: "true"
        run: |
          # ビルドされたイメージのダイジェストを取得して署名を実行
          # 署名情報はイメージと同じレジストリに .sig というサフィックスでプッシュされる
          cosign sign --yes ghcr.io/your-org/your-app@${{ steps.build-and-push.outputs.digest }}

2. Kubernetesクラスタ側での検証の強制(Admission Controller)

署名されたイメージであっても、デプロイ時に検証されなければ意味がない。Kubernetesクラスタ側で「署名のないイメージの実行を拒否する」ポリシーを強制する必要がある。これには Kyverno や Connaisseur などのAdmission Controllerを使用する。

以下は、Kyvernoを用いたポリシーの定義例だ。このポリシーは、指定したレジストリからデプロイされるイメージに対して、特定のパブリックキー(またはOIDCアイデンティティ)による署名が存在することを強制する。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce # 検証に失敗した場合はデプロイをブロック(Enforce)
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/your-org/*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    # 署名を行ったGitHub ActionsのリポジトリやIssuerを検証
                    subject: "https://github.com/your-org/your-app/.github/workflows/build.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: "https://rekor.sigstore.dev"

このポリシーが適用されているクラスタでは、たとえ攻撃者がレジストリのタグを書き換えて不正なイメージを指すように工作したとしても、KyvernoがSigstoreの透明性ログと証明書を突合し、正当な署名がないことを検知してPodの作成を即座に拒絶(Reject)する。

—

チーフホワイトハッカーの視点:導入の罠と監査の勘所

ここまで読んだ実務担当者は、「よし、明日から導入しよう」と思うかもしれない。しかし、現場のインフラを預かるセキュリティアーキテクトとして、あえて泥臭い現実と落とし穴を警告しておこう。

1. イメージのダイジェストとタグの乖離
Kubernetesのマニフェスト(Deployment等)において、イメージを指定する際は必ずタグ(latest や v1.0.0)ではなく、ダイジェスト(@sha256:....)で指定するべきだ。タグは指し示す先を後から書き換えることが可能であるため、署名検証のタイミングとデプロイのタイミングで競合状態(TOCTOU: Time-of-Check to Time-of-Use)が発生する余地を与えてしまう。
2. 社内プライベートレジストリでのRekor/Fulcio運用
インターネットから完全に隔離された(エアギャップ環境の)金融機関や重要インフラでは、パブリックなSigstoreインフラ(sigstore.dev)は利用できない。この場合、社内にFulcio、Rekor、Tessellationといったコンポーネントをオンプレミスで構築・運用するコストが発生する。その見極めを誤ると、プロジェクトが頓挫する。
3. 例外処理(Break-glass)の設計
障害時に緊急パッチを当てるため、署名プロセスをバイパスしなければならない状況(ブレークグラス)は必ず訪れる。この緊急時の手順を厳格な監査ログの監視下におきつつ、誰がどの権限でバイパスできるのかを設計していないと、セキュリティの名の下に現場の運用が完全にマヒする。

—

結びにかえて:信頼を「信じるな、検証しろ」

ゼロトラストセキュリティの基本原則は「Never Trust, Always Verify(決して信頼せず、常に検証せよ)」である。これはネットワークやアイデンティティの話だけにとどまらない。

あなたがデプロイする一桁のバイト列、一本のコンテナイメージに至るまで、その「出自」と「完全性」を暗号学的に証明し、実行の瞬間に検証する仕組みを持たない限り、本当の意味でのサプライチェーン防衛は達成できない。

脆弱性スキャンで穴を塞ぐ作業は「対症療法」に過ぎない。Cosignをはじめとするコード署名基盤の導入は、インフラストラクチャ全体に「揺るぎない信頼のアンカー」を打ち込むための、現代のエンジニアにとって必須の防衛装備なのだ。今日からあなたのパイプラインとクラスタを見直し、すべてのイメージに cryptographic な証跡を義務付けてほしい。

コメント

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