T-CREATOR

Docker イメージ署名と検証:cosign でサプライチェーンを防衛する運用手順

Docker イメージ署名と検証:cosign でサプライチェーンを防衛する運用手順

Docker コンテナがますます普及する一方で、その安全性を確保することが重要な課題となっています。特にサプライチェーン攻撃への対策は、企業のセキュリティ戦略において欠かせない要素です。

本記事では、Sigstore プロジェクトの cosign を使った Docker イメージの署名と検証手順を詳しく解説します。cosign を導入することで、イメージの改ざん検知や信頼性の担保が可能となり、サプライチェーンを防衛できるようになるでしょう。

背景

コンテナイメージのセキュリティリスク

コンテナ環境では、パブリックレジストリから取得したイメージをそのままデプロイすることが一般的です。しかし、イメージの出所や内容が本当に信頼できるかを確認する仕組みがなければ、以下のようなリスクに晒されます。

  • イメージが改ざんされている可能性
  • 悪意のあるコードが埋め込まれている危険性
  • ビルド過程での不正な操作
  • 中間者攻撃によるイメージのすり替え

以下の図は、従来のコンテナイメージの配信フローを示しています。

mermaidflowchart LR
  dev["開発者"] -->|ビルド| img["Docker イメージ"]
  img -->|プッシュ| registry["コンテナレジストリ"]
  registry -->|プル| prod["本番環境"]
  attacker["攻撃者"] -.->|改ざん?| registry
  attacker -.->|中間者攻撃?| prod

このフローでは、レジストリやネットワーク経路で攻撃者がイメージを改ざんする可能性があり、本番環境に不正なコードが混入するリスクが存在します。

Sigstore プロジェクトと cosign の登場

Sigstore は、ソフトウェアのサプライチェーンセキュリティを強化するために Linux Foundation が主導するオープンソースプロジェクトです。その中核ツールである cosign は、コンテナイメージに対してデジタル署名を行い、検証することで、イメージの完全性と真正性を保証します。

cosign の主な特徴は以下の通りです。

#特徴説明
1鍵ペア方式秘密鍵で署名し、公開鍵で検証
2Keyless 署名OIDC 認証を利用した鍵管理不要の署名
3OCI レジストリ対応既存のコンテナレジストリと互換性あり
4軽量で高速Go 言語で実装され、CI/CD パイプラインに組み込みやすい

cosign を使うことで、開発者が署名したイメージのみを本番環境にデプロイする運用が実現できます。

課題

イメージの信頼性を担保する難しさ

Docker イメージの信頼性を確保するには、以下の課題をクリアする必要があります。

1. 改ざん検知の仕組み

イメージがレジストリに保存された後、誰かが内容を書き換えたとしても、従来の仕組みでは検知が困難です。SHA256 ハッシュ値でイメージを特定することはできますが、そのハッシュ値自体が正しいかを保証する手段がありません。

2. 署名鍵の管理

デジタル署名を行うには秘密鍵が必要ですが、その鍵を安全に保管・管理することは容易ではありません。鍵が漏洩すれば、攻撃者が偽の署名を作成できてしまいます。

3. CI/CD パイプラインへの組み込み

署名と検証のプロセスを既存の CI/CD ワークフローに統合するには、自動化の仕組みが必要です。手動での運用は現実的ではなく、開発速度を損なわずにセキュリティを強化する必要があるでしょう。

以下の図は、署名と検証が欠けた場合の課題を示しています。

mermaidflowchart TD
  build["イメージビルド"] --> push["レジストリへプッシュ"]
  push --> question1{"改ざん<br/>されていない?"}
  question1 -->|不明| pull["プル"]
  pull --> question2{"本当に正規の<br/>イメージ?"}
  question2 -->|不明| deploy["デプロイ"]
  deploy --> risk["セキュリティリスク"]

このように、各段階で信頼性を確認する手段がないため、リスクを抱えたままデプロイせざるを得ない状況となります。

サプライチェーン攻撃の脅威

近年、ソフトウェアサプライチェーンを標的とした攻撃が増加しています。2021 年の SolarWinds 事件や、2022 年の Log4Shell 脆弱性など、信頼されているコンポーネントが侵害されることで、広範囲に被害が拡大する事例が後を絶ちません。

コンテナイメージも例外ではなく、以下のような攻撃経路が考えられます。

  • ビルド環境への侵入によるマルウェア混入
  • レジストリアカウントの乗っ取り
  • ベースイメージへのバックドア埋め込み

これらの脅威に対抗するには、イメージの出所を明確にし、改ざんを検知できる仕組みが不可欠です。

解決策

cosign による署名と検証の基本フロー

cosign を導入することで、Docker イメージに対して暗号学的な署名を付与し、その署名を検証することで信頼性を保証できます。基本的な運用フローは以下の通りです。

以下の図は、cosign を使った署名・検証フローを示しています。

mermaidflowchart LR
  dev["開発者"] -->|ビルド| img["Docker イメージ"]
  img -->|署名| signed["署名付きイメージ"]
  signed -->|プッシュ| registry["レジストリ"]
  registry -->|プル| verifier["検証プロセス"]
  verifier -->|公開鍵で検証| result{検証結果}
  result -->|成功| deploy["デプロイ"]
  result -->|失敗| reject["拒否"]

この仕組みにより、署名されていないイメージや改ざんされたイメージは検証段階で弾かれ、安全性が確保されます。

cosign の 2 つの署名方式

cosign には、鍵ペアを使った署名と Keyless 署名の 2 つの方式があります。

鍵ペア方式

秘密鍵と公開鍵のペアを生成し、秘密鍵で署名を行い、公開鍵で検証します。鍵の管理は必要ですが、オンプレミス環境や厳格なセキュリティ要件がある場合に適しています。

Keyless 署名

OIDC(OpenID Connect)認証を利用し、鍵の管理を不要にする方式です。GitHub Actions や GitLab CI などの CI/CD 環境で、開発者の ID を証明として署名を行います。鍵の保管リスクを回避でき、運用が簡素化されるでしょう。

#方式メリットデメリット
1鍵ペア方式完全な制御が可能鍵の管理が必要
2Keyless 署名鍵管理不要、運用が簡単OIDC プロバイダーへの依存

本記事では、まず鍵ペア方式での基本的な運用手順を解説し、その後 Keyless 署名の方法も紹介します。

署名と検証がもたらす安全性

cosign による署名と検証を導入することで、以下のセキュリティ効果が得られます。

  • 完全性の保証: イメージが改ざんされていないことを確認
  • 真正性の保証: イメージが正規の開発者によって作成されたことを証明
  • 監査証跡: 誰がいつ署名したかを記録し、追跡可能に
  • ポリシー適用: 署名されていないイメージのデプロイを自動的に拒否

これらの効果により、サプライチェーン攻撃のリスクを大幅に低減できます。

具体例

環境準備

cosign を使った署名と検証を行うための環境を準備します。以下の手順では、ローカル環境での検証を想定していますが、CI/CD パイプラインにも同様に適用できます。

cosign のインストール

cosign は Go 言語で実装されており、各プラットフォーム向けのバイナリが提供されています。

macOS の場合、Homebrew を使ってインストールできます。

bashbrew install cosign

Linux の場合は、以下のコマンドでバイナリを直接ダウンロードします。

bashcurl -Lo cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign
sudo mv cosign /usr/local/bin/

インストール後、バージョンを確認しましょう。

bashcosign version

正常にインストールされていれば、バージョン情報が表示されます。

Docker のインストール確認

Docker がインストールされていることを確認します。

bashdocker --version

まだインストールしていない場合は、Docker 公式サイトから最新版を入手してください。

鍵ペアの生成

cosign で署名を行うには、まず秘密鍵と公開鍵のペアを生成する必要があります。

以下のコマンドで鍵ペアを生成します。

bashcosign generate-key-pair

実行すると、パスワードの入力を求められます。このパスワードは秘密鍵を保護するために使用されるため、安全なものを設定してください。

textEnter password for private key:
Enter password for private key again:

生成が完了すると、以下の 2 つのファイルが作成されます。

#ファイル名説明
1cosign.key秘密鍵(署名に使用)
2cosign.pub公開鍵(検証に使用)

重要: cosign.key` は絶対に外部に漏らさないように管理してください。Git リポジトリにコミットしたり、公開レジストリにアプロードしたりしないよう注意が必要です。

秘密鍵を安全に保管するために、以下のような方法を検討しましょう。

  • 暗号化されたストレージに保存
  • KMS(Key Management Service)を利用
  • CI/CD の Secrets 機能で管理

Docker イメージのビルドとプッシュ

署名対象となる Docker イメージを準備します。ここでは簡単なサンプルアプリケーションを使います。

サンプル Dockerfile の作成

以下の内容で Dockerfile を作成します。

dockerfileFROM node:18-alpine

WORKDIR /app

# アプリケーションの依存関係をコピー
COPY package*.json ./

# 依存パッケージをインストール
RUN npm ci --only=production

# アプリケーションコードをコピー
COPY . .

# アプリケーションを起動
CMD ["node", "index.js"]

このシンプルな Dockerfile は、Node.js アプリケーションをコンテナ化します。

イメージのビルド

イメージをビルドします。ここでは myapp:v1.0.0 というタグを付けます。

bashdocker build -t myapp:v1.0.0 .

ビルドが完了したら、イメージが作成されたことを確認しましょう。

bashdocker images | grep myapp

レジストリへのプッシュ

署名を付与する前に、イメージをコンテナレジストリにプッシュします。ここでは Docker Hub を例としますが、AWS ECR や Google Container Registry など他のレジストリでも同様に動作します。

まず、Docker Hub にログインします。

bashdocker login

次に、イメージにレジストリのタグを付けます。yourusername は自分の Docker Hub ユーザー名に置き換えてください。

bashdocker tag myapp:v1.0.0 yourusername/myapp:v1.0.0

レジストリにプッシュします。

bashdocker push yourusername/myapp:v1.0.0

これでレジストリにイメージが保存されました。次のステップで、このイメージに署名を付与します。

イメージへの署名

cosign を使ってイメージに署名を付与します。

以下のコマンドで署名を実行します。

bashcosign sign --key cosign.key yourusername/myapp:v1.0.0

実行すると、秘密鍵のパスワード入力を求められます。

textEnter password for private key:

パスワードを入力すると、署名プロセスが開始されます。

textPushing signature to: yourusername/myapp

署名が完了すると、署名データがレジストリに保存されます。cosign は署名情報をイメージと同じレジストリに保存するため、別途署名ファイルを管理する必要はありません。

署名の確認

署名が正しく付与されたかを確認するには、以下のコマンドを実行します。

bashcosign tree yourusername/myapp:v1.0.0

以下のような出力が表示されれば、署名が正常に保存されています。

text📦 yourusername/myapp:v1.0.0
└── 🔐 Signatures
    └── sha256:abc123...

この出力から、イメージに署名が付与されていることが確認できます。

署名の検証

署名されたイメージの検証を行います。検証には公開鍵を使用します。

以下のコマンドで検証を実行します。

bashcosign verify --key cosign.pub yourusername/myapp:v1.0.0

署名が有効であれば、以下のような JSON 形式の出力が表示されます。

json[
  {
    "critical": {
      "identity": {
        "docker-reference": "index.docker.io/yourusername/myapp"
      },
      "image": {
        "docker-manifest-digest": "sha256:abc123..."
      },
      "type": "cosign container image signature"
    },
    "optional": null
  }
]

この出力には、署名されたイメージの詳細情報が含まれています。検証が成功したことで、イメージが改ざんされておらず、正規の秘密鍵で署名されたことが証明されました。

もし検証に失敗した場合は、以下のようなエラーが表示されます。

textError: no matching signatures

このエラーが出た場合、イメージが署名されていないか、署名が改ざんされている可能性があります。

CI/CD パイプラインへの組み込み

実際の開発現場では、署名と検証を CI/CD パイプラインに組み込むことで、自動化された安全なデプロイフローを構築できます。

GitHub Actions での署名例

GitHub Actions を使った自動署名の例を示します。

以下の内容で .github​/​workflows​/​build-and-sign.yml を作成します。

yamlname: Build and Sign Docker Image

on:
  push:
    branches:
      - main

jobs:
  build-and-sign:
    runs-on: ubuntu-latest

    steps:
      # リポジトリをチェックアウト
      - name: Checkout code
        uses: actions/checkout@v3

次に、Docker のセットアップを行います。

yaml# Docker のセットアップ
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

# Docker Hub にログイン
- name: Login to Docker Hub
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKER_USERNAME }}
    password: ${{ secrets.DOCKER_PASSWORD }}

イメージをビルドしてプッシュします。

yaml# イメージのビルドとプッシュ
- name: Build and push Docker image
  uses: docker/build-push-action@v4
  with:
    context: .
    push: true
    tags: yourusername/myapp:${{ github.sha }}

cosign をインストールして署名を実行します。

yaml# cosign のインストール
- name: Install cosign
  uses: sigstore/cosign-installer@v3

# イメージに署名
- name: Sign Docker image
  env:
    COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
  run: |
    echo "${{ secrets.COSIGN_KEY }}" > cosign.key
    cosign sign --key cosign.key yourusername/myapp:${{ github.sha }}
    rm cosign.key

この設定により、main ブランチにプッシュされるたびに、イメージが自動的にビルドされ、署名が付与されます。

重要: COSIGN_KEYCOSIGN_PASSWORD` は、GitHub の Secrets 機能で安全に管理してください

Kubernetes での検証例

Kubernetes 環境では、署名されていないイメージのデプロイを防ぐために、Admission Controller を使った検証が有効です。

Sigstore Policy Controller を使うことで、署名検証を自動化できます。

Policy Controller をインストールするには、以下のコマンドを実行します。

bashkubectl apply -f https://github.com/sigstore/policy-controller/releases/latest/download/policy-controller.yaml

次に、検証ポリシーを定義します。以下の内容で policy.yaml を作成します。

yamlapiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-images
spec:
  images:
    - glob: 'yourusername/*'
  authorities:
    - key:
        data: |
          -----BEGIN PUBLIC KEY-----
          # ここに cosign.pub の内容を貼り付け
          -----END PUBLIC KEY-----

このポリシーを適用します。

bashkubectl apply -f policy.yaml

これにより、yourusername 配下のイメージは署名検証が必須となり、検証に失敗したイメージはデプロイが拒否されます。

Keyless 署名の利用

鍵管理の負担を軽減するために、Keyless 署名を利用することもできます。Keyless 署名では、OIDC 認証を利用して開発者の ID を証明します。

Keyless 署名の実行

GitHub Actions で Keyless 署名を行う例を示します。

yaml# Keyless 署名
- name: Sign Docker image (Keyless)
  run: |
    cosign sign yourusername/myapp:${{ github.sha }}

この方法では、秘密鍵を管理する必要がなく、GitHub の OIDC トークンを使って署名が行われます。

Keyless 署名の検証

Keyless 署名の検証には、証明書情報を指定します。

bashcosign verify \
  --certificate-identity=https://github.com/yourorg/yourrepo/.github/workflows/build.yml@refs/heads/main \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  yourusername/myapp:v1.0.0

この検証により、特定のワークフローから署名されたイメージのみを信頼することができます。

署名付きイメージの運用フロー全体像

以下の図は、cosign を使った署名と検証の運用フロー全体を示しています。

mermaidsequenceDiagram
  participant Dev as 開発者
  participant CI as CI/CD
  participant Reg as レジストリ
  participant K8s as Kubernetes
  participant PC as Policy Controller

  Dev->>CI: コードをプッシュ
  CI->>CI: イメージをビルド
  CI->>Reg: イメージをプッシュ
  CI->>CI: cosign で署名
  CI->>Reg: 署名を保存
  K8s->>Reg: イメージをプル
  K8s->>PC: 署名検証を要求
  PC->>Reg: 署名を取得
  PC->>PC: 公開鍵で検証
  alt 検証成功
    PC->>K8s: デプロイ許可
    K8s->>K8s: Pod を起動
  else 検証失敗
    PC->>K8s: デプロイ拒否
    K8s->>K8s: エラーを記録
  end

このフローにより、署名されていないイメージや改ざんされたイメージは自動的に弾かれ、安全性が担保されます。

トラブルシューティング

cosign の運用中に発生しうるエラーと解決方法を紹介します。

エラー 1: Error: signing [yourusername/myapp.0.0]: getting signer: reading key: no such file or directory

発生条件: 秘密鍵ファイルが見つからない場合に発生します。

解決方法:

  1. 秘密鍵ファイル cosign.key が正しいパスに存在するか確認
  2. --key オプションで正しいパスを指定
bashcosign sign --key /path/to/cosign.key yourusername/myapp:v1.0.0

エラー 2: Error: verifying [yourusername/myapp.0.0]: no matching signatures

エラーコード: 検証エラー

発生条件: イメージに署名が付与されていないか、公開鍵が間違っている場合に発生します。

解決方法:

  1. イメージに署名が付与されているか確認
bashcosign tree yourusername/myapp:v1.0.0
  1. 正しい公開鍵を使用しているか確認
  2. 署名を再度実行してみる

エラー 3: Error: failed to verify signature: crypto/rsa: verification error

エラーコード: 署名検証エラー

発生条件: 署名と公開鍵が一致しない場合に発生します。

解決方法:

  1. 署名時と検証時で同じ鍵ペアを使用しているか確認
  2. イメージが改ざんされていないか確認
  3. 鍵ペアを再生成して署名をやり直す

まとめ

Docker イメージの署名と検証は、サプライチェーンセキュリティにおいて極めて重要な対策です。cosign を導入することで、イメージの完全性と真正性を保証し、改ざんや不正なコードの混入を防ぐことができます。

本記事では、cosign の基本的な使い方から、CI/CD パイプラインへの組み込み、Kubernetes での検証まで、実践的な運用手順を解説しました。鍵ペア方式と Keyless 署名の両方を理解することで、環境や要件に応じた最適な運用が可能となるでしょう。

署名と検証の仕組みを導入することで、以下のメリットが得られます。

  • イメージの改ざん検知
  • 正規の開発者によるイメージであることの証明
  • サプライチェーン攻撃への対策強化
  • 監査証跡の確保

セキュリティは一度導入して終わりではなく、継続的に運用・改善していく必要があります。cosign を活用して、安全なコンテナ環境を構築していきましょう。

関連リンク