- 真野隼記
- 約 3,100 文字
- 500 View
目次
CNCF連載の1本目です。
はじめに
TIG真野です。KubeCon + CloudNativeCon の日本初開催、おめでとうございます!
CNCFのIncubatingプロジェクトである、Notary v2を用いて、コンテナイメージにデジタル署名してみた記事です。
CI/CDを狙ったサプライチェーン攻撃が増えている昨今、デプロイ対象のコンテナイメージが正しい手順で作成されたか検証することの重要性はますます高まっているように感じます。業界でよく行われているコンテナイメージの署名とその検証がどのように行われているか興味が合ったため、OCI(Open Container Initiative)準拠でデファクトに近い位置づけであったNotaryを触ってみした。
Notary v2とは
Notaryは、コンテナイメージにデジタル署名し、その真正性(誰が作ったか)と完全性(改ざんされていないか)を保証し、CLI実装が Notation です。Notaryは現在v2です。特徴として署名データをイメージと同じレジストリ内に保存することがあり、これによりポータビリティが高いと言われます。v1時代はレジストリをまたいでイメージを移動させると、署名が失われるという課題があったそうで、再設計されたためv2になったそうです。
2024年7月にコンテナイメージ署名用の AWS Signer オープンソース Notation プラグイン - AWS という発表があった通り、Notary v2 のCLI実装である Notation に、AWS Signerのプラグインが公開されたため、面倒な鍵管理をAWS側に任せることもできるようになりました。利用の機運が高まります。
ちなみに、類似のイメージ署名ツールとしては、Sigstore(Cosign)も有名で、勢いがあるかもしれません。こちらはGitHubなどのOIDCアカウントで署名するという、キーレスが特徴とのこと。記事を書いている途中で気がついたのですが、こちらの方が面白そうなツールですね。
Notaryの想定される使い所
Notary(実際に用いるのはCLI実装のNotation)の使い所ですが、以下のようなフローを想定しています。
- CI上でtrivyなどのイメージスキャンを行い、合格した場合にNotationで署名する
- CDのタイミングで、Notationで署名を検証して、成功ステータスの場合のみデプロイする
- もし、検証が失敗した場合はデプロイを失敗させ、野良イメージ(or 悪意のあるイメージ)のデプロイを防ぐ
ある開発者がローカルPCでビルドしたイメージを、ECRなどのレジストリにプッシュしてしまうことは、適切なIAM権限設定で防ぐことができます。しかし、インフラ構築用の踏み台サーバ経由などでプッシュされる可能性もゼロではありません。また、CI/CDパイプライン上で設定ミスや予期しないスクリプトが実行され、ECRにイメージがプッシュされてしまう可能性も絶対発生しないとは言い切れません。
セキュリティ対策は多層的になされることが原則であるため、リスク評価にも続き優先度付けの上で、こういった対策を講ずるべき場合もあるでしょう。
より詳しくは、ECS × AWS Signer を使ったイメージ署名ワークフローを試してみた の記事が参考になりました。
Notationのインストール
Install the notation CLI に従い、Notationをインストールします。
x86かつ、WSL2上の環境にて構築します。 notationはGo言語で開発されていますが、go install ではお手軽には無理そうだったため、バイナリを直接取得する流れにします。
export NOTATION_VERSION=2.0.0-alpha.1 |
コンテナレジストリを起動
DockerHubにプッシュしてもよいですが、単なる技術検証で用いるのは迷惑だと思ったので、ローカルPC上にレジストリを起動させます。同じく、CNCFのSandboxプロジェクトの、 Zot という軽量OCIレジストリを利用します。
インストール手順を見ると、がやや面倒だったのと、揮発的な利用でしか利用しないためDockerコマンドで立ち上げます。
docker run -d -p 5000:5000 --name zot ghcr.io/project-zot/zot-linux-amd64:v2.1.4 |
通常は、HTTPSでしかプッシュできないのですが、この Zotレジストリと通信するため、Docker Desktopの設定を修正します。
Docker DesktopのGUIから、歯車マーク > Docker Engine で、以下の設定を追加。
{ |
そのまま、DockerをApply & Restartで再起動します。
もし、zotが再起動していなければ、再度立ち上げて起きます。
docker start zot |
テストキーと自己署名証明書を生成する
署名するためのテスト RSA キーと、検証するための自己署名 X.509 証明書を、以下で生成します。あくまで動作確認用のコマンドとのこと。プロダクションで利用時はこちらの手順を参考にして、 AWS Signer などと連携させるか、少なくても、AWS KMSなどシークレットの管理サービスを利用すべきでしょう。
notation cert generate-test --default "suji-toshi-mashoya" |
notation key ls や notation cert ls で署名キーや証明書を確認できます。
イメージに署名
適当なイメージをpullし、ローカル上のZotレジストリにpushします。
docker pull busybox:1.37.0 |
続いて、お待ちかねのnotationコマンドです。タグだと上手く見つけることができなかったため、ダイジェスト指定することが必要でした(最近だと、タグは書き換え可能であり、ダイジェストを使う方が良いという話もあるので、深入りはしていません)
$ notation sign --insecure-registry "localhost:5000/suji-tootteruyo@sha256:7c0ffe5751238c8479f952f3fbc3b719d47bccac0e9bf0a21c77a27cba9ef12d" |
以下で紐づきを確認できます。
notation ls localhost:5000/suji-tootteruyo@sha256:7c0ffe5751238c8479f952f3fbc3b719d47bccac0e9bf0a21c77a27cba9ef12d |
署名の検証
信頼ポリシーという、どの署名を信頼するか定義したJSONを作成します。
cat <<EOF > ./trustpolicy.json |
JSONファイルをインポートします。
notation policy import ./trustpolicy.json |
署名を検証します。
notation verify --insecure-registry "localhost:5000/suji-tootteruyo@sha256:7c0ffe5751238c8479f952f3fbc3b719d47bccac0e9bf0a21c77a27cba9ef12d" |
無事、成功しました。
署名の検証が正しく失敗するか確かめる
信頼ポリシーに登録されていない、不正な鍵で署名されたイメージがプッシュされた場合に、 notation verify による署名検証が失敗することを確かめます。
まず不正なキーを生成します。
notation cert generate-test --default "fade-out" |
前回のキーと、今回の攻撃用のキーの2種類存在します。 fade-out が攻撃用のキーです。
新しく別のイメージを準備します。前回利用した1.37.0 ではなく、1.36.0を利用します。
docker pull busybox:1.36.0 |
このイメージに対して、攻撃用のキーで署名します。
notation sign \ |
これで、信頼できない署名付きのイメージが作成されましたので、このイメージを検証します。
notation verify --insecure-registry localhost:5000/suji-tooranaiyo@sha256:49fc8cc2e956660bb8c6ab9cd18618609eb106ae5503855e7b2b3de5138c7ec6 |
想定通り、「署名の検証に失敗しました:アーティファクト localhost:5000/suji-tooranaiyo@sha256:… に適用可能な信頼ポリシーのルールが存在しません。」といったエラーがでて、検証が失敗しました。
さいごに
Notary v2 のCLI実装である、Notationを使ってコンテナイメージの署名と検証をしました。
署名および検証はNotationのコマンドで容易に実行でき少し拍子抜けしました。難しいポイントは鍵の管理かと思いますが、それもNotationの AWS Signerプラグインなどを上手く活用することで、マネージドサービス側に寄せられると運用も楽になるのだろうと推測しています。
Notationの使い方というか、コンテナの指定がタグではなくダイジェストでないと認識しないといったあたりで時間がかかりました。CIなどで自動化しようとすると、シェルスクリプトなどで吸収するかといった手間が必要そうです。