電通総研 テックブログ

電通総研が運営する技術ブログ

GitHub Actions で distroless イメージのコンテナ署名を検証する

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。
CI/CD環境として GitHub Actions を使っているときに、コンテナをビルド、プッシュする前に distroless ベースイメージの署名を検証する方法について紹介します。(自前でビルドしたイメージの署名やその検証については、この記事では扱いません)

(2023/8/21追記)Cosign 2.0よりKeyless Signingが推奨されるようになっているため、記事の一部を更新しました。

はじめに

コンテナを利用する場合、軽量で最小限のファイルのみを含んでいる distroless イメージをベースイメージとして使うことが推奨されます。
distroless イメージは Cosign で署名されており、GitHub リポジトリの README でも、イメージの使用前に署名を検証することが推奨されています

All distroless images are signed by cosign. We recommend verifying any distroless image you use before building your image.

Cosignとは

Cosign はコンテナなどを署名するためのライブラリであり、OSSコミュニティである Sigstore が提供するツールの1つです。Sigstore は2022年10月にGAを発表しました。

distroless イメージは全て Cosign の Keyless signing で署名されています。

署名の検証で期待したいこと

コンテナのベースイメージの署名を検証することで、イメージが改ざんされておらず(完全性)、検証時に指定する公開鍵とペアの秘密鍵で署名されていること(真正性)が期待できます。イメージが不正に差し替えられた場合には、それを利用する前に検知できるようになります。

一方、署名用の秘密鍵への正当なアクセスを持つユーザーや、アクセス権を入手した攻撃者がベースイメージを置き換えた場合、署名も書き換えることが可能になるので、署名の検証による防御の対象外になります。また秘密鍵にアクセスできなくても、検証に使用する公開鍵を差し替えできてしまう場合も、署名の検証による防御の対象外です。

このような攻撃の成立を難しくするために、Keyless signing が推奨されています。Keyless signing の流れは次の通りです。

  1. 署名者が OIDC (GoogleGitHubなど)で認証する
  2. 署名者は発行されたOIDCのIDトークンを利用し、署名に利用する鍵ペアを生成する
  3. 生成した公開鍵とIDトークンをSigstoreの Fulcio に送信する
  4. FulcioはIDトークンを検証し、短命の証明書(公開鍵が含まれる)を発行する
  5. 署名者は署名を行い、証明書と署名をSigstore の Rekor という透明性ログサービスに記録する
  6. 秘密鍵は破棄される
  7. 署名検証時では、利用者はRekorから証明書およびそれに含まれる公開鍵を取得し、証明書が有効だった期間に署名が行われたことを検証する

この仕組みにより、公開鍵を自前で公開する手段を用意する必要はなく、秘密鍵も短命のものを利用し署名後は破棄されるため、盗まれるリスクも大幅に低減されます。また仮にOIDCの認証が突破されたとしても署名の記録はRekorにタイムスタンプ付きで公開されるため、仕組みとしては身に覚えのない署名を検知できるようになっています。
なお署名の流れが複雑に見えますが、そのほとんどはCosignのCLIコマンドで自動化されます。

cosign verify コマンド

さて cosign verify コマンドで署名者のEメールアドレスと、署名者が利用したOIDCプロバイダーを指定すると署名の検証ができ、例えば distroless/nodejs:18 イメージに対しては次のように出力されます

$ cosign verify gcr.io/distroless/nodejs:18 --certificate-oidc-issuer https://accounts.google.com --certificate-identity keyless@distroless.iam.gserviceaccount.com

Verification for gcr.io/distroless/nodejs:18 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate was verified using trusted certificate authority certificates
(以下略)

Cosign による署名と検証についてこちらの記事で非常に詳しく説明されています。

cosign verify は何をやっているのか

cosign による署名と検証について、自分なりの理解をまとめると次のようになります。

  • コンテナイメージを保管する OCI(Open Container Initiative) Registry には、Manifest、Config、Blob の3種の Artifact を保存できる
  • Manifest ファイルには Config や Blob(コンテナのレイヤー)への参照がダイジェストで記載されており、従ってコンテナレイヤーが変わるとイメージの Manifest ファイルも変わる
  • Cosign による署名はイメージの Manifest ファイルに対して作成される
  • Cosign でコンテナイメージを署名すると、イメージの Manifest ファイルのダイジェストを Signature Claim という JSONファイルに埋め込む
  • Signature Claim も Artifact として OCI Registry にアップロードされる。この署名の Manifest ファイルには、Signature Claim のダイジェストを秘密鍵で暗号化したものが signature として記載されている

イメージの署名

  • 署名の検証は、まずイメージの Manifest ファイルから特定の規則に従い、署名の Manifest ファイルの格納場所を算出する。これにはイメージの Manifest ファイルのダイジェストが利用される
  • 署名の Manifest ファイルに書かれた signature を公開鍵で復号し、Signature Claim のダイジェストと一致しているかを検証する

署名の検証

検知したいと思っている「コンテナイメージをすり替えたが、署名と公開鍵をすり替えることができない」状況を想像します。イメージレイヤーが変わるとイメージの Manifest ファイルも変更になり、 Manifest ファイルのダイジェストから算出される署名の格納場所も変わります。しかし攻撃者はその場所に従来の秘密鍵で署名を格納することができない前提で考えているため、結果としてコンテナイメージのすり替えを検知できます。

追加の検証をするべき状況

cosign verify だけでは十分ではなく、追加の検証をした方が良い状況も存在します。それは「イメージ署名の検証」と「実際のイメージのpull」に時間的な差分があるときです。cosign verify はコンテナのダイジェストを直接検証に使用するわけではないため、cosign verify する時点の OCI Registry にあるイメージと、pull してきたイメージが同一ではない可能性が無視できない場合、docker inspect コマンドで pull してきたイメージのダイジェストと署名に記載されたダイジェストの比較が必要です。

署名を検証するワークフロー

さて前置きが長くなりましたが、ここからはイメージ署名の検証とイメージ pull の時間差を無視して良く、追加の検証を行わない場合を考えます。
GitHub Actions でコンテナのビルドやプッシュを行う前に、以下のステップを追加することで distroless ベースイメージの署名を検証できます。

    steps:
      # 自身のリポジトリのチェックアウト。署名の検証とは無関係
      - name: Checkout
        uses: actions/checkout@v3
      # 1. Cosign のインストール
      - name: Install cosign
        uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
      # 2. Cosign による検証。例として nodejs:18 イメージのものを対象としている
      - name: cosign verify
        run: cosign verify gcr.io/distroless/nodejs:18 --certificate-oidc-issuer https://accounts.google.com --certificate-identity keyless@distroless.iam.gserviceaccount.com
      # これ以降コンテナのビルドやプッシュを行う

説明:

  1. sigstore/cosign-installer アクションを利用し、cosign をインストールする
  2. --certificate-oidc-issuer--certificate-identity オプションを指定し、Keyless署名されたコンテナイメージを検証する
    • ここでは gcr.io/distroless/nodejs:18 イメージを検証している

署名の検証にかかる時間

cosign のインストールと署名の検証はいずれも数秒以内に終わるため、特にCI/CDの妨げにはならない印象です。

署名を検証するワークフロー

数行のアクションを追加するだけでベースイメージが不正にすり替えられていないか検証できるため、万が一に備えて distroless イメージの署名検証をやってみてはいかがでしょうか。


私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。

セキュリティエンジニア(セキュリティ設計)

執筆:@kou.kinyo、レビュー:寺山 輝 (@terayama.akira)Shodoで執筆されました