サプライチェーンセキュリティにおける脅威と対策の再評価

本ブログの主旨

  • サプライチェーンセキュリティにおいて既存のフレームワークよりも具象化されたモデルを用いて脅威及び対策を精査することで、実際のプロダクトへのより実際的な適用可能性及び課題を検討した。
  • 具象化されたモデルにおいては「脅威の混入箇所と発生箇所が必ずしも一致しない」という前提に立ち、各対策のサプライチェーンセキュリティにおける位置付け及び効力を検討した。とりわけ、ともすれば無思考的に採用しかねないSBOM等の「流行の」対策に対して、その課題や効果の限定性を明らかにした。
  • これらの脅威分析に基づき、「サプライチェーンの構成要素に存在する多数の開発者それぞれに対して責任を分散して負わせる」形態のパイプラインを置き換えるものとして、「各構成要素に存在する開発者に対して一定の制約を強制する代わりに、サプライチェーンセキュリティに関するオペレーションを一点に担う中央化されたCIパイプライン」の必要性を提案した。

メルカリのセキュリティエンジニアリングチームでインターンをしている橋本(@smallkirby)です。
皆さんはサプライチェーンセキュリティという言葉を聞いたことがあるでしょうか。近年のソフトウェアサービスは、開発者がコードを書いてからそれをビルド・リリース/デプロイするまでの間に多くの自動化された工程(CI/CDパイプライン)を辿り、その間に多くの依存関係を含んでいます。サプライチェーン攻撃は、このパイプライン上の各工程において直接・間接的に攻撃を仕掛けるものです。サプライチェーン攻撃の件数は年々増加しており、その深刻さ・影響度も大きくなってきています。

メルカリにおいてもそれは例外ではなく、多数のサービスが様々なCI/CDツールを用いており、それらは多くの依存関係の上に成り立っています。これは、サプライチェーンに対する攻撃可能性を多分に含んでいるということを意味します。実際、2021年4月に発生したCodecovインシデントではメルカリも直接的な被害を受け、社内セキュリティの見直しと改善を迫られました。

サプライチェーンの見直しと再検討

上記の状況を受けて、様々なコミュニティがサプライチェーンにおいて満たすべき要件をまとめたフレームワークを提案しています。有名なところで言うと、GoogleやLinux Foundationの人々が中心となって立ち上げたSLSAや、Aqua SecurityとCISが共同で発表したCIS Software Supply Chain Security Guide等が挙げられます。これらは、多くのソフトウェア会社が採用するようなCI/CDモデルの各段階において実際に適用できるようなセキュリティ上の要件をまとめており、とりわけSLSAでは要件に4つのレベルを持たせることによって段階的にプロダクトに適用できるようになっています。
しかしながら、これらのフレームワークはまだ発表された直後であり、実際のプロダクトに適用された実績や経験が十分にありません。SLSAはアルファステータスであり、現在も仕様に未確定な部分や検討が必要な箇所が複数あり、議論が交わされている最中です。

メルカリのサプライチェーンセキュリティを向上させるにあたって、これらのフレームワークを適用するには以下の点を再評価する必要があります:

  • SLSA等で想定されるCI/CDパイプラインモデルが、メルカリの持つパイプラインに当てはまるかどうか
  • 提案されている要求事項によって、想定される攻撃手法は全てカバーできるかどうか。またそのためにも、考え得る攻撃手法・攻撃可能性が全て列挙し尽くされているか
  • あるセキュリティ要件は、攻撃手法に対してどのような位置付けの防御策となるのか
    要求事項を、実際のパイプラインにどのように落とし込んで適用できるのか

とりわけ、ある要求事項がどの脅威に対してどのような防御策となり得るのかを今一度評価することは大切であると考えられます。SLSAやCIS等を採用しようとした場合、確かに要求事項はある程度明確に一覧として示されているためエンジニアリングコスト・マネジメントコストこそかかるものの採用自体に大きな障壁はあまりないでしょう。

しかしながら、要求事項をただ鵜呑みにして採用していると、どの要求事項がどのような脅威に対してどの程度の防御効果を持っているのかを見失いやすく、これさえやっていれば完璧だという銀の弾丸であるかのように錯覚してしまう危険性があります。サプライチェーン攻撃において絶対に、漏れなく、完全なる防御となるような対策はほとんど(全く)無く、ある種の多層防御の一つにすぎません。そのため、各対策の立ち位置を今一度しっかりと判断することが重要です。よって、まずは発生し得る脅威を全て洗い出し、ある対策がその脅威に対してできること、できないこと、その効果範囲や程度を洗い出すことが必要であると考えます。

本ブログではまずメルカリにおける、ひいては近年のサプライチェーンモデル全般におけるCI/CDの実装とそれに対する脅威を改めて洗い出します。その後、それら脅威に対する対策を既存のフレームワークを踏まえた上で再評価します。最後に、考えられる対策を組み込み、かつそれらの管理をアプリケーションの開発者から引き離した中央集権的CIパイプラインのPoC的な設計について例を挙げ、実プロダクトに対する応用について評価していきます。

近年の典型的なCI/CDパイプラインモデル

DevOpsが進んだ近年の開発環境において、一般的なCI/CDパイプラインのモデルを以下に示します:

パイプラインモデル

このモデルでは、コードリポジトリとしてGitHubを、CI環境としてGitHub Actionsを、CD環境としてKubernetes(GKE等)上で動くArgoCDを使っています。但し、上図で利用されているソフトウェアはあくまでも一例であり、以降言及するThreat ModelやMitigationは各コンポーネントで違うソフトウェアを使っていても当てはまることがほとんどです(例: コードリポジトリにGitLab、CI環境にCircleCIを利用している等)。

脅威の混入箇所と発火箇所

以下では、このモデルにおいて発生し得る脅威をThreat Modelとしてまとめ、それに対するMitigationを考えていきます。なお、図中の[A]のようなインデックスはパイプライン上のコンポーネントの識別子であり、本ブログではThreat ModelやMitigationをこのコンポーネントごとにまとめています。

サプライチェーン攻撃においては、サプライチェーンが多くの構成要素から成り立っているという性質上、脅威(malice)が混入/発生するコンポーネントと、注入された脅威が発火して実際に影響を及ぼす箇所が必ずしも一致しないという場合が多くあります。

例として、攻撃者がGCRへの書き込み権限を不正に奪取し、正当なCIパイプラインの外からコンテナイメージをGCRにpushしたと仮定します。この場合、脅威が発生している箇所はGCR、脅威が発火する箇所はそのイメージがpullされてデプロイされる本番環境のKubernetes上であるということができます。
多層防御を本質とするサプライチェーン攻撃対策においては、脅威の混入を防ぐだけではなく、パイプラインに含まれている脅威を途中で検知することで最終配布物(サービス及びソフトウェア本体)から脅威を排除するという観点も重要になります。そのため、以下では脅威の混入箇所と発火箇所を明確にしつつ、脅威の混入を防ぐ対策に加えて脅威の検出及び追跡を可能にするための対策についても考慮するものとします。

サプライチェーン攻撃における攻撃手法とMitigation概要

想定したCI/CDパイプラインにおいて考えられるThreat ModelをこちらのPDFにまとめました。続く章ではこれらの攻撃手法について軽く触れていきます。

Threat Modelの概要

本章では、各コンポーネントにおけるThreat Modelの大まかな概要のみをお伝えします。その後、続く章で特筆すべき脅威や観点のみ詳しく触れていきます。各Threatは先ほど示したパイプラインモデルのコンポーネントごとにまとめられています。なお、以下に示すコンポーネントごとの観点は概要であり、詳細についてはこちらのPDFをご覧ください。

クリックするとThreat Modelの概要を表示します:

以下のThreat Modelは、現在の一般的な開発フローを利用するパイプラインを想定しています。例として、ソースコードの管理にはSCMを用いていたり、CIは開発者の端末ではなくGitHub Actions等のEphemeralなコンテナ環境を利用していること等は暗黙の前提としています。

A. Source Repository (commits)

開発者がソースコードを書いてからソースコードリポジトリにpushするまでの間の区間です。なお、CIフェーズ(ソフトウェアのコード本体)とCDフェーズ(Kubernetesのマニフェスト)の両方においてこのコンポーネントが存在します:

1. 攻撃者がfeatureブランチに対して悪意ある変更をpush

攻撃者がSSH鍵・GitHubアカウント・開発者端末等を掌握した後、それらを用いて悪意ある変更をfeatureブランチにpushします。攻撃者がSSH鍵・GitHubアカウント・開発者端末等を掌握した後、それらを用いて悪意ある変更をfeatureブランチにpushします。

2. 攻撃者がmainブランチに対して悪意ある変更をpush

攻撃者がSSH鍵・GitHubアカウント・開発者端末等を掌握した後、それ等を用いて悪意ある変更をmainブランチにpushします。mainブランチに対して有効なBranch Protection Ruleが設定されていない場合や、Adminアカウントを掌握することで特権的にmainブランチへの書き込みを行うこと等が考えられます。

3. 古いリポジトリやブランチ等のスキャン

攻撃者が入手したread権限を用いて、古いリポジトリやブランチ等のスキャンを行います。セキュリティ要件が低い時代であったり、メンテナンスが放棄されているようなリポジトリで、且つ本番環境に対していまだにアクセスできたり秘匿情報を含んでいる場合には攻撃の起点とされる可能性があります。

B. Source Repository (Review)

変更に対するPRが作成され、mainブランチに対してmergeされるまでの区間です。なお、SCMプラットフォーム自体の掌握(たとえばGitHubプラットフォーム全体が攻撃者に奪取された場合)は本ブログのカバー範囲外です:

1. 攻撃者自身によるSelf-Approval

攻撃者が作成したPRに対して、自分自身でApproveすることでレビュー要件をバイパスします。なお、GitHubにおいてはPRを作成した本人がApproveを行うことはできません。

2. 掌握した複数のアカウントを用いてのApproval

攻撃者が作成したPRに対して、他に掌握したアカウントを用いてApproveすることでレビュー要件をバイパスします。GitHubにおいては、PRを作成するのに用いるアカウントの他にApproveをするアカウントが別途必要になります。但し、インサイダーによる攻撃を想定する場合には追加で掌握するアカウントは1つで十分になります。

3. ApproveされたPRに対する追加の変更

攻撃者が悪意のない正当な変更に対してPRを作成し、正当なレビュアーからApproveを受けた後、悪意のある変更をPRに対してpushします。GitHubの設定において追加のcommit後にApproveが破棄されるようになっていない場合、レビュー要件をバイパスしてコードをマージすることができます。

4. Bug-Door (一見すると悪意のないコード)

攻撃者が、一見すると悪意が無いが特定の条件下においては攻撃が成立するようなコードをpushします。レビュアーがこれを見抜けなかった場合、レビュー要件を満たした上で悪意のあるコードがmainブランチにマージされます。Linux Kernelにおいて発生したHypocrite Commitと同様のケースです。また、一般には直接レビューされにくいような自動生成のコード(gRPCのブロブコードやpackage.lockのようなファイル)に対して悪意のある変更を及ぼすような場合もこのケースに含みます。

5. プラットフォームにおけるレビューバイパス機構の悪用

ソースリポジトリプラットフォームにおいて特定の条件下でレビューをバイパスできるような状況が発生する場合、攻撃者がこれを悪用してレビュー要件をバイパスしつつ悪意あるコードをmainブランチにマージできる可能性があります。

6. ボット/Appアカウントの悪用

PRに対してボットアカウント/Appがなんらかの操作を行う場合、攻撃者がこれらの挙動を悪用してレビュープロセスをバイパスする可能性があります。特定のパスへの小さな変更(ドキュメントの追加やタイポの修正等)をするようなPRに対して自動的にApproveを行うようなApp等が考えられます。

7. Adminアカウントの掌握によるレビュー要件の変更

攻撃者がAdminアカウントを掌握し、リポジトリのBranch Protection Rule等を変更することでレビュー要件を回避しつつmainブランチに変更をマージします。

C. CI (test, build, CI toolings)

feature/mainブランチ上でのCI環境(GitHub Actionsなど)内における区間です。なお、GitHub Actions Runnerの掌握は本ブログの対象範囲外となります:

1. CIツールによるコードへの不正な変更

LinterなどのCIツールがリポジトリへの書き込み権限を持っており、且つこれらのツールによるcommitがレビューを必要としない場合、攻撃者はCIツールを侵害することで悪意のあるコードを注入できる可能性があります。

2. 意図しない参照先の利用

CIプロセスにおいて外部リソースを依存として利用し、且つリソースの参照がmutable(入力値を外部から与えることができる等)であるような場合に、攻撃者が悪意ある参照先をActionsに利用させることができる可能性があります。
また、GitHubリポジトリのRepository Redirection機能を悪用し、削除されたアカウント及びリポジトリを攻撃者が取得することで悪意あるコードを含むリポジトリを利用させることができる可能性があります。

3. 改変されたActionsの実行

GitHub Actionsでは、PRを作成したブランチのActions定義ファイル(/.github/workflows/*.yml)を変更した場合、変更後の定義に基づいてActionsを実行します。featureブランチに対して不適切にSecretsや権限を付与していた場合、攻撃者がそれらの権限を用いて任意の処理を実行できる可能性があります。

4. CIのバイパス

CI内で静的テストを行う場合に、攻撃者はSCMプラットフォームの特例条件を悪用してCIをバイパスできる可能性があります。GitHubの場合、コミットメッセージに[skip ci]を含めたり、Actions定義ファイルにpathsコンディションを含める等が考えられます。

5. Actionsに対する悪意あるInputs

GitHub Actionsはブランチ名やPRコメント等を入力として取ることが可能です。また、Actions定義内でGit CLIを呼び出し、さらなる入力値を取得することも考えられます。このような場合に、攻撃者はこれらの入力値を細工し、不正な参照先を利用させたり不正な処理を行わせることができる可能性があります。

6. テストコードの改変

CI環境においてビルドしたソフトウェアに対してE2Eテストを行い、実際の外部サービスと連携するような場合には、テストコードに対して悪意ある変更をpushすることで外部サービスに対して攻撃を行ったりSecretを奪取できる可能性があります。

7. CIステップ間の相互作用

GitHub Actionsにおいては、jobは原則としてパラレルに、stepはシーケンシャルに実行されます。あるstepが後続のstepに対して処理結果を渡す場合に攻撃者があるstepの結果を不正に操作し、後続のstepを掌握できる可能性があります。また、パラレルに実行されるjobが互いに作用することのできる状況の場合、攻撃者があるjobを起点として他のjobに悪意ある操作を行うことができる可能性があります。

D. Secrets / Dependency

CI環境において、とりわけ依存関係やSecretを利用する区間です:

1. CIツール等による環境変数の奪取

GitHubにおいて、Secretとして登録した値は環境変数に渡すことでActionsステップ内で利用することができます。該当Actionsに悪意あるCIツール等が利用されている場合、環境変数内のSecretを奪取することができる可能性があります。Codecovインシデントと同様のケースです。GitHubにおいて、OIDCトークンやEnvironmentsを利用せず全ブランチに対して適用されるSecretsを利用している場合にはリスクが高まります。

2. 過剰なシークレットの付与

GitHub Actionsにおいて、featureブランチ上で走るCIに対して本番環境のSecretを利用することができるようになっている場合、攻撃者はActions定義を書き換えることで本番環境の権限を利用して処理を行うことができる可能性があります。

3. 掌握された依存関係の利用

攻撃者が何らかの方法で依存パッケージを掌握し悪意あるコードを注入することで、依存を利用するCIに任意の処理を行わせることができる可能性があります。悪意あるコードの注入方法としては、大まかに以下のパターンが考えられます:

  • Artifact(ベースコンテナイメージやビルド時の依存パッケージ等)を配布するサーバを掌握することで、任意のバイナリ・コードを該当パッケージとして配布する。パッケージがハッシュ値を公開しており且つそのハッシュ値も改ざんできるような場合、ハッシュ値も改ざんすることでハッシュ値検証を不可能にする。
  • 依存パッケージのソースリポジトリに悪意ある変更を加えることで、ビルド後のArtifactが悪意ある挙動をするようにする。この場合、ハッシュ値検証では防げない。
  • 依存パッケージが依存するパッケージに対して悪意あるコードを注入する。それを元にしてビルドされた依存Artifactを利用する。この場合、ハッシュ値検証では防げない。
  • 正当なリポジトリのオーナーが悪意あるコードを注入しArtifactをビルド・公開する。color.jsやfaker.jsにおいて起こった事件と同様。この場合、ハッシュ値検証では防げない。

4. 意図しない依存関係の利用

利用する依存関係やCIツール等への参照を攻撃者が書き換えることで意図しない依存関係を利用する可能性があります。

5. コンテナレイヤー内の秘匿情報

秘匿情報がArtifactであるコンテナレイヤー内に含まれる場合、攻撃者が秘匿情報を奪取できる可能性があります。Google Cloud Buildでは.gitignoreや.gcloudignoreファイルに明記されていない限りワークスペースのすべてのファイルを含むようにビルドされるため、そこにセンシティブな情報が含まれてしまうような場合が考えられます。

E. OCI registry

CI環境で生成されたArtifact(コンテナイメージ)をpushし、保存している区間です:

1. Featureブランチからproduction環境のコンテナレジストリへのpush

FeatureブランチのCI環境に対して過剰な権限が与えられている場合、攻撃者がActions定義を書き換える等の方法で任意のイメージをコンテナレジストリにpushできる可能性があります。CI から Container Registry へのアクセスに際して OIDC 等を用いたIdentity Federationを行っている場合に、不適切なGCPのWorkload Identityの設定によって、意図しないブランチのCI環境から本番環境のContainer Registryに対してイメージがpushされる可能性があります。

2. 意図しないイメージのpush

攻撃者がActions定義を書き換える等の方法でビルドプロセスを改変した場合、正常のビルドプロセス以外の方法でビルドされたイメージがコンテナレジストリにpushされる可能性があります。

3. 奪取したアカウントを用いてコンテナレジストリ内のイメージの書き換え

攻撃者が奪取したGCPアカウントを用いてコンテナレジストリに対して悪意あるイメージをpushすることで、CDパイプラインにおいて悪意あるイメージの利用を強制できる可能性があります。

F. CDプロセス

Kubernetesのマニフェストが更新され、ArgoCD等のCDツールが新しいマニフェストをapplyするまでの区間です。この区間はCDパイプラインの構成によってコンポーネントの差異が大きいため、一般に適用できるThreat Modelの紹介にとどめます。なお、マニフェストの変更部分は”A. Source Repository (commit)”と”B. Source Repository (review)”と同様です:

1. 奪取したWebhookを用いた不正なSyncリクエスト

ArgoCDにおいてマニフェストリポジトリのポーリングを無効化し、明示的なWebhookによってマニフェストのsyncを行っている場合、攻撃者が奪取したWebhookシークレットを用いて不正にsyncリクエストを送ることができる可能性があります。この際、sync対象のブランチやタグに制限がない場合には攻撃者が任意のバージョンに対してsyncを行わせることができる可能性があります。

2. 同一クラスター内の他のサービスに対するマニフェストの適用

マニフェストリポジトリが複数のサービスの情報を管理するモノリポジトリであった場合、あるサービスのマニフェストによって他のサービスを書き換えることができる可能性があります。これは、ArgoCDが全サービスに対する強い権限を保有し、かつデプロイ時に適切にサービスアカウントのImpersonationを行わない場合にリスクが高まります。また、Branch Protection Ruleが不十分である場合には攻撃者が他のサービスのマニフェストをソースリポジトリ上で書き換えることも考えられます。

3. 意図しないイメージのPull

攻撃者がマニフェスト内のイメージリファレンス(タグ等を含む)を書き換えた場合、攻撃者が用意した悪意あるイメージの利用を強制される場合があります。

4. Kubernetesの奪取したシークレットを利用した直接的なデプロイ

攻撃者がKubernetsの運用者アカウントを奪取した場合、マニフェストリポジトリ内のリポジトリとは関係なく任意のサービス等をデプロイする事が可能です。

(Threat Modelの概要おわり)

Mitigationの概要

それぞれのThreat Modelに対するMitigationの詳細については、同じくこちらのPDFを参照ください。各脅威に対して以下の視点から可能なMitigationを列挙しています:

  • Prevention
    • Injection Prevention: 悪意あるコード等がそもそもパイプラインに注入されることを防ぐ
    • Trigger Prevention: 悪意あるコード等が発火し、動作し始めるのを防ぐ
    • Impact Reduction: 悪意あるコード等が発火した後、影響が拡大・深刻化するのを防ぐ
  • Detection: インシデントが発生したことを検知する
  • Traceability: 過去に遡って過去のパイプラインの構成要素の状態を追跡することができる

このようにMitigationを分類しているのは、各個の対策はサプライチェーンのセキュリティ”全体”を”完全”に保証するものではないという事を前提としているためです。それぞれの対策は、”脅威の混入箇所と発火箇所”の章でも説明した通り、いずれかの側面において限定的な効果を持つに過ぎません。どの対策がどのような範囲においてどのような効果を持つのかということを明示的に整理することで初めて実際に運用できるセキュリティ要件を決定できると考えます。

特筆すべきMitigation

本章では、提示したThreat Modelに対する対策をジャンルごとにまとめ、それらの性質・できる事・関連ツール状況等を可能な限り紹介していきます。
前述したThreat Modelは大きく分けると「アカウントやトークンの不正な奪取と悪用」をするものと「依存関係を侵害することによりサプライチェーン内に侵入する」ものの2種類があると言えます。以下に示すMitigationの多くは、この前者の攻撃経路に対して「最終的な成果物が意図したパイプラインのフローを辿ってきたことを保証・強制する」ことによって最終的な脅威の発火を防ごうとしています。一方で、後者の依存関係の侵害に関しては完全に食い止めることは難しく、その意味でもTraceabilityという観点が重要であるということがわかると思います。

このような「脅威の混入箇所と発火箇所が異なる」という事実は既存のフレームワークにおいては強調されていないながらも、各対策の立ち位置と効力を正確に認識し、適切な対策を構えるためには必要不可欠なことであると考えられます。以下の節は、Mitigationが混入の防止と発火の防止のどちらでどれだけの効果をもつのかを意識しながら読み進めていただけると幸いです。

Source Repository

ソースリポジトリにおける最大の課題は、悪意あるコードがソースコードに注入されることを防ぐことです。これは、OSSのように広く公開されたリポジトリだけではなく、企業内のプライベートリポジトリにおいても当てはまることで、企業外部者からの攻撃の他にインサイダーによる攻撃も十分に考慮する必要があります。
原則として、write権限を持たない攻撃者がリポジトリに対してpushを行うためには、まず正規の開発者の端末やアカウント・鍵を乗っ取る必要があります。よって、リポジトリへの不正pushを防ぐmitigationは以下のようなことが考えられます:

  • Two Factor Authentication(2FA)の強制
  • 適切なBranch Protection Ruleの作成、及びadminアカウントを含めた全アカウントへの適用
  • Adminユーザの数の制限
  • コミットのサインの強制
  • Inactiveなユーザアカウントの削除

より厳密に制限をかけるとすると、ソースリポジトリにアクセス可能なIPアドレスを制限する等のネットワーク制限も考えられます。2FAやBranch Protection Ruleに関しては、GitHubの設定から強制することができます。コミットのサインに関しては、GitHub OIDCトークンを用いてkeylessにサインを行うGitsign等の利用が考えられます。

続いて、リポジトリ上でのコードレビュー周りの対策です。
攻撃者はレビューされること自体や、レビューを受けた上で悪意あるコードがrejectされることをバイパスしようとしてきます。それらに対するmitigationとしては以下のようなことが考えられます:

  • Two Person Reviewの強制 (branch protection rule / CODEOWNERS)
  • 2FAの強制

SLSAやCIS等のフレームワークで共通して強調されていることとしてTwo Person Reviewがあります。他には、PRに対してApprovalをもらった後に追加で変更をpushした場合にはApprovalを無効にするという設定もあります。但し、これは開発スピードに影響する要素でもあります。commitの場合と同様に、より厳密な制限を課す場合にはネットワーク制限を行うことも考えられます。

Secret

CI環境が保持するSecretとしては、CIツールが該当リポジトリを閲覧するためのGITHUB_TOKEN等の他、ビルドしたイメージをContainer RegistryにPushするためのシークレットや他の外部サービスと連携するためのシークレットが考えられます。
静的な鍵を利用する際の注意事項には、以下が挙げられます:

  • シークレットに必要最低限の権限のみを持たせる
  • シークレットを利用することのできるビルドプロセスのスコープを狭める
  • 鍵のローテーションや生存期間を短くする等の一般的な対策
  • ネットワーク制限

シークレットが利用できるスコープに関して、GitHub Actionsの場合にはStep単位で制限する事ができます。しかしながら、あるfeatureブランチに対して攻撃を受けActionsの定義ファイルが自由に変更できる状態になってしまった場合、攻撃者によってシークレットを奪取される可能性があります。このような場合にはGitHubのEnvironment Secretを使うことで、ブランチごとに利用できるシークレットを制限する事ができます。また、あるシークレットを利用するようにActionsを変更した場合には指定のレビュアーに対してレビューを要求するようにすることもできます。
これらの静的なシークレットと比較して、OIDCを利用した場合には鍵の流出リスクをより小さく抑えられることが期待できます。GitHubではOIDCトークンを用いて外部サービス(GCPやAWS等)のリソースにアクセスする事ができ、外部サービスに該当Job期間のみ有効なトークンを発行してもらう事ができます。

また、より厳格さを求める場合はネットワーク制限を課すことも考えられます:

上図では必要な依存関係のインストールをビルドの前に全て行った後、ビルドの際には一切のネットワークアクセスを禁止します。ビルド後にContainer Registryにイメージをpushする際には再びネットワークアクセスを許可します。このような方法をとることで、ビルドプロセス内で悪意あるコード(改変されたbuild.rs等)が動いていた場合でも不正な依存関係を注入されたり、ビルド時の情報を不正に外部にリークされることを防ぐ事ができます。これを後続のテストやCIツール等にも拡張させ、これらのタスクでネットワーク制限(もしくはAllowlist)を行うことで該当タスクが利用するシークレットのネットワーク越しの漏洩リスクを軽減することも期待できます。但しこれを実際に適用することには困難も伴います。実際、普通のGitHub ActionsではStep/Jobごとにネットワーク制限を行うことはできません。Self-hostedなGitHub ActionsをKubernetes上で動かしている場合には、Istioなどを利用してこれらを実現することが考えられます。

Signing

署名の検証は、サプライチェーンセキュリティにおける「発火」を防ぐ上で最も基本的で重要な要素の一つです。Artifactの生成時にCI環境から署名を行い、Kubernetesからイメージをプルする際に署名の検証を行うことで、署名を行った(= Artifactのビルドを行った)のが信頼できるEntityであることが保証されます。攻撃者が直接Container Registryに対して悪意あるImageをpushした場合には、そのImageに対応する署名が存在しないため、デプロイ時に署名検証が失敗することで悪意あるイメージのデプロイを防ぐことができます。

ここで問題となるのが、鍵の管理です。鍵の管理は、とても面倒なのでできればやりたくないものです。そこで使えるのがSigstoreのcosignです。cosignでは、GoogleやGitHub等のOIDCトークンを利用して認証を行い、keylessで署名を行うことができます。公開鍵・秘密鍵はそれぞれ生成するにはしますが、秘密鍵は署名のために一度のみ利用し、公開鍵はOCIレジストリ及びRekorが管理するTransparency Logに載せておくことができるため、それらを管理する必要はありません。

ただし、Transparency Logのパブリックインスタンスは誰でも閲覧可能、且つ削除・編集不可能という性質上、企業のようなdomesticな活動を公にしたくない場合には組織内にプライベートインスタンスを建てる必要があります。GCP KMSのようなプライベートなキーマネージサービスを利用する場合には、Workload Identityを適切に設定することでサービスごと、ブランチごとに対応する鍵の利用権限を与えるようにする事ができます。keylessを用いる場合も従来の静的な鍵を利用する場合にも、正当な承認されたビルド環境のみが署名鍵にアクセスできるようにする必要があります。

Signingの検証

Signingは付与しただけでは意味はなく、デプロイ時に利用しようとしているイメージが正しい署名を持っているかどうかを検証し、その成否に応じてデプロイするかどうかを決定する必要があります。実装方法としては、CDパイプラインの途中にイメージの検証フェーズを入れる方法や、KubernetesのAdmission Controllerという形で検証プロセスを入れる等の方法が考えられます。
後者の方法として、sigstoreが開発しているPolicy Controllerを利用することができます。Policy Controllerは、Policyという形で特定(若しくは全て)のイメージに対して署名が必須であることを宣言し、Policyが満たされない場合にはイメージのデプロイを禁止することができます。このようなツールを用いることで、イメージが確かに信頼するビルド環境からpushされたものであることをデプロイ時に検証することができ、仮にパイプラインのどこかのタイミングで悪意あるイメージがコンテナレジストリに書き込まれたとしても発火を食い止めることができます。
なお、署名によって保証されることは「KubernetsがPullしたイメージが、署名鍵へのアクセス権限を持つ正当なビルド環境によってビルドされPushされたコンテナイメージそのものである」という事実のみであり、ビルドプロセスそのものが意図された通りに動作したことや、依存関係に対して脆弱性が存在しないことは一切保証しないということは留意しておく必要があります。

依存関係の追跡

全てのソフトウェアは多くの依存関係の上に作られています。これらの依存関係先のセキュリティまで担保することは非常に難しく、依存の利用時に脅威の混入を完全に防止することは非常に困難であると考えられます。ゆえに、ここで重要になってくるのが依存関係の追跡(Traceability)という観点です。

近年になって頻繁に聞くようになってきたSBOM(Software Bill of Materials)も、サプライチェーンセキュリティのTraceabilityという観点において一定の効果が期待されています。SBOMは、コンテナイメージやソフトウェアパッケージが依存するパッケージ等を一覧化したリストのことを指します。JavaScriptによるアプリケーションやGo言語製のアプリケーションの場合にはソースリポジトリ内のpackage.lockやgo.sumを見ることでSBOMなしにアプリケーション自体の依存を一覧する事ができますが、SBOMにはこれらとは異なる点がいくつか存在します。

まず、言語固有の形式ではなく標準化された形式を用いる事ができるため、サービスごとに異なる言語を用いていた場合にも統一的にパッケージを管理する事が可能になります。また、コンテナイメージを作成する場合にはベースとなるイメージを利用することが多く、ベースイメージがSBOMを提供している場合にはそれらの情報も統合して最終的な依存リストの中に含めて管理する事が可能となります。このようにベースイメージも含めた依存関係を明示的に管理できると、あるパッケージにおいてゼロデイ脆弱性が発見された場合に該当パッケージを利用しているイメージを容易に特定して対応できることが期待できます。

SBOMの生成

SBOMの生成を行うツールはまだ発展途上の段階です。現在のところ利用可能なツールとして、GoogleがCNCFのsandboxプロジェクトとして開発するkoや、Chainguard社が開発するapko等が挙げられます。koはGo言語製のアプリケーションに対して自動的にSBOMを付与してくれる一方、apkoはビルド済みの任意言語のバイナリに対してディストロレス(-like)なコンテナをSBOMとともに生成してくれるという違いがあります。他には、Aqua Security社のコンテナスキャナであるTrivyもSBOMを生成してくれるコマンドを持っています。
これらの生成はビルドプロセスの一環として行い、OCIレジストリにコンテナイメージと同様に置いておく事ができます。SBOMのpushにもCosignを用いる事ができます。

SBOMの限界

まず、SBOMはその特性上先述したMitigationの分類におけるTraceabilityに分類され、脆弱性の混入そのものに対して大きな防止効果を期待することはできません。後述するAttestationと組み合わせることで、SBOMが示すパッケージに対して何らかのポリシーを定義し特定のソースからのパッケージを禁止すること等は一応可能です。しかし、基本的には依存パッケージに新たに脆弱性が見つかった際に、依存を利用しているイメージ一覧を統一的に検索する等の目的で利用する事が多いと思います。

また、そもそも現状SBOMで追跡することのできないファイルが存在するということにも留意する必要があります。コンテナイメージのSBOMを作成する際、若しくはそれらの脆弱性スキャンを行う場合は、OSの標準パッケージマネージャのDBが利用されます。Go言語等のシングルアプリコンテナの場合には、それらのDBとgo.mod等のファイルを組み合わせてコンテナ全体のSBOMを生成する事ができます。しかし、Dockerfileの中でcurlやwget等のコマンドを用いて依存をインストールした場合、それらは(ほとんどの場合)SBOM生成ツールやスキャナによって追跡する事ができません。依存関係をベースイメージまで遡って透過性を得るためには、このような隠れたパッケージを追跡するような方法が必要ですが、現在のツール等ではこの機能はあまり存在していないという認識です。

また、SBOMで依存関係を追跡できていたとしても、その依存関係が正当なものであるかどうかを評価できるかというのはまた別の話です。もちろん、依存先のパッケージが改ざんされていないかどうか、それをビルドしたのが正当なビルダーであるかという点は、CosignやRekorを用いた署名検証や後述のAttestationによって保証することが出来ます。しかしながら、全ての依存先がこのようなセキュアなフローを辿る事を必要条件とすることは現実にはできません。ある依存パッケージが依存する先を辿っていくと、ほぼ必ずと言って良いほどセキュリティ要件を満たしていないパッケージが出てくるはずです。このような依存先の正当性を保証する事は現状では非常に困難ですが、以下に取り得る候補を挙げて本節を締めくくります:

  • Assured OSS, Google: Googleのセキュアなパイプラインを用いてビルド及び継続的にファジングされたOSS群。2022年8月現在、250パッケージを提供
  • Wolfi, Chainguard: ディストロレスなベースイメージの提供
  • Security Scorecards, OpenSSF: リポジトリのセキュリティ要件に対するスコアづけ

Attestation

Signingの章でも言及したように、Signing自体は「Pullしようとしているイメージが正当な署名鍵へのアクセス権限を持ったビルド環境によってPushされたこと」を保証するに過ぎません。そのイメージがどのようなビルドフローで、どのような引数を受け取り、どのような情報を持っているかということを消費者(consumer又はverifier)に伝えるためには、Attestationを利用する事ができます。

Attestationはビルド時にイメージとは別に生成し、(一般には)イメージと同様にOCIレジストリに格納することで機能します。Attestation(Provenance)に用いるフォーマットは現在標準化の途中という段階ですが、今の段階ではin-totoフォーマットが有名なところだと思います。Attestationは、subjectによってアテスト対象のイメージを同定します。Attestationをイメージに付与することで、Artifactがどのようにして生成されたのかを記述する事ができます。これらのAttestationに対しても署名を施すことで、署名鍵へのアクセス権限を正当に保持したビルド環境が、Attestationに記述されている内容でビルドしたと主張したことを保証する事ができます。

Attestationに含めることのできる内容

Attestationの中に含めることのできる内容は広大であり、実質的には何でも格納する事ができます。例えば、cosignを用いることでSBOMをAttestationとして付与する事ができます:

$ cosign attach sbom –k8s-keychain –sbom $SBOM $IMAGE

また、コンテナスキャナーの結果をAttestationとして付与することもできます。Cosignはin-totoフォーマット以外のPredicateタイプとしてGeneric Predicate Specificationを定めており、その1つとして脆弱性スキャン結果を記述するvulnというPredicateタイプを持っています。これを利用して、Trivyが生成したスキャンレコードをAttestationとしてイメージに付与する事が簡単に可能です:

$ cosign attest --key gcpkms:// --type vuln --predicate trivy.vuln.json $IMAGE

その他にも、Cosignではcustomタイプを指定することで任意のデータを持つPredicateを作成することが可能です。

複数のAttestationを作成してイメージに付与(OCIレジストリにPush)した場合、それぞれのAttestationは1つのレイヤーとして表現され、それぞれが異なるタイプ・フォーマットを持つ事ができます。

Attestationと検証

Attestationを作成したあとは、Signingの章と同様にAttestationの内容について検証を行う事ができます。Cosign自体がAttestationに対して検証を行うことのできる仕組みを持っています(これは、Attestationの署名に対する検証ではなくAttestationが記述する内容に対するセマンティックな検証の事を指しています)。ルールはCUE言語かRegoで記述する事ができます。

また、先述したPolicy Controllerを用いてデプロイ時にKubernetesのAdmission Controllerでポリシーを検証することも可能です。例として、あるイメージが指定のKMSで管理される鍵で署名されており、且つTrivyによるコンテナスキャンの結果CVSS 8.0以上の脆弱性がある依存関係が存在しないことを保証するルールは以下のように記述できます:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: trivy-scan
spec:
  images:
    - glob: gcr.io//simple-server*
  authorities:
    - name: gcp
      key:
        kms: >-
          gcpkms://<KEY>
      attestations:
        - name: cvss-less-than-8
          predicateType: vuln
          policy:
            type: cue
            data: |
              predicateType: "cosign.sigstore.dev/attestation/vuln/v1"
              predicate: {
                scanner: {
                  uri: =~ "pkg:github/aquasecurity/trivy@.*"
                }
              }

              #Vuln: {
                CVSS: {
                  nvd: {
                    V3Score: number & <8.0
                    ...
                  }
                }
              }
              result: {
                ArtifactType: "container_image";
                Results: [...{
                  Vulnerabilities: [...#Vuln]
                }]
              }

このようにAttestationとポリシーの検証を組み合わせることにより、イメージのビルド環境や依存パッケージに対する制限をデプロイ時に強制する事が可能です。上で示した例以外にも、先述したGitHubにおけるTwo Person Reviewの強制や、コミットのサインの強制等もAttestationに含めた場合、それらもポリシーとして記述できるようになるかもしれません。

中央化されたCIパイプラインのPoC例

以上の要件を満たすようなパイプラインを構築したとしても、社内で運用するにあたっては問題があります。それは、複数サービス/アプリケーションが独自のビルドシステムを用いている場合には推奨するセキュリティ要件を満たせないという事です。

各サービスがGitHub Actions内で独自にアプリケーション・コンテナをビルドし、それをContainer Registryにそれぞれプッシュするというような仕組みになっていた場合、各サービスにAttestationのフォーマットを強制させること等が難しくなります。最悪の場合には、各サービスの開発者がAttestationを任意に操作してあたかもポリシーを満たしているかのように偽造することも可能になってしまいます(Attestationの署名はあくまでも正当なビルドシステムがAttestationを作成したことを保証するものであるため、各サービス内の正当なビルドシステム内でAttestationを作成する場合、その署名鍵も各サービスが保有することになり、Attestationは任意に作成する事が可能です)。
そこで、以下のような中央化・標準化されたGo言語アプリのビルドシステムを構築する事が考えられます:

このパイプラインではビルドシステムとしてKubernetes上で走るTekton Pipelineを利用しています。おそらく(Self-hosted) GitHub Actionsでも実現可能とは思いますが、関連ツーリングとの親和性から今回はTektonを例に紹介します。

まず、各サービスの開発者はソースコードの静的テスト等はソースリポジトリに紐づくGitHub Actions上で行います。但し、各サービスのリポジトリにはほとんど静的なシークレットを与えません。これによって、各サービスが自由にできる領域においてセキュリティホールが生じたとしても機密情報の漏洩リスクを最低限に抑える事ができます。それらのテスト後、GitHubからTektonに対してビルドリクエストを送ります。TektonではInterceptorと呼ばれる仕組みで発行者の検証を行う事ができます。

リクエスト発行者の検証後、TektonのTaskと呼ばれる単位でタスクを実行していきます。なお、タスク内にはStepが定義され、それぞれのStepは別々の隔離されたコンテナ上で動作します。

タスクの最初のステップにおいて、依存関係を全てダウンロードします。その後、必要であればネットワークを完全に(もしくは部分的に)遮断した上でArtifactをビルドし、署名を行います。続いてコンテナスキャナをビルドしたイメージに対してかけ、その結果をAttestationとして生成・署名します。必要であれば、さらにカスタムのAttestationをここで作成することも可能です。最後に、生成したArtifactとAttestation及び署名を全てContainer Registryにpushします。

なお、Tekton Chainsを用いることでSLSAが定めるようなProvenanceを持つin-totoフォーマットのAttestationを自動で作成する事ができます。ChainsはContainer RegistryへのイメージのPushを自動的に検知し、該当Task/Pipelineワークフローに関するAttestationを生成して作成及び署名してくれます。

図ではイメージやAttestationの署名でKMSで管理される鍵を利用していますが、必要に応じてFulcio+Rekorを利用したKeyless署名を行うことも可能です。

以上が中央化されたCIパイプラインとしてとりえる形の一例です。もちろんこれを実際に運用するとなると、先述したThreat Model全てをカバーできているかどうかの他に、DXを大きく損なわないか、運用上の技術的問題点はないかを再考する必要があります。開発者がDockerfileをビルドのエントリポイントとして指定した場合、そのDockerfile内における操作(依存関係のインストール等)を追うことが出来ないという問題点があります。また、CD側では上記のCIパイプラインで生成したAttestationや署名類を適切に検証するフローを加える必要が出てきます。

しかしながらここで最も強調したいことは、「開発者が『特に変なことをせずに何も考えずに使えば安全(Secure By Default)』であるようなCIパイプラインの抽象化層」を用意する必要があるという事です。正直その中で行うステップ(Attestationの付与やその内容等)については二の次であり、必要となれば随時その抽象化層に要件を足していくことで全てのサービスにその利用を強制する事ができるという環境自体が重要であると考えられます。

まとめ

本ブログでは、具象化されたパイプラインモデルを想定し、そこに介在し得る攻撃可能性とそれに対するMitigationについて考えていきました。これはよく言われることですが、セキュリティ及びサプライチェーンセキュリティにおいては、どれをとっても万全であると言い切ることのできる対策はありません。

SBOMは万能ではないし、Attestationは全てを保証してはくれません。そのどれもが多層防御における一防壁を成すものであると考えられます。だからこそ、SLSA等のフレームワークのように必要要件を明確に定義して防壁を可能な限り厚くすることが重要です。但しこの領域はまだまだ発展途上であり、今現在も盛んに議論が交わされ、要件が見直され、関連ツールが次々と産み出されていっています。そのため、提唱されているフレームワークを盲目的に過信し、これをしているから万全であると思い込むことは危険であるように思えます。

今一度自分たちが使っているパイプラインを見直し、提唱されている要件がどのような攻撃可能性に対して、どのような対策となり得るのか、どのようなことは防げないのか、どのようにして実環境に適用して運用することができるのかをゼロベースで再考することは、必要なプロセスであり且つ意味のあることでしょう。

このようなサプライチェーンセキュリティの見直しは様々なコミュニティが行なっており、それぞれが独自の観点から評価を行なっています。どれが正解とかどれが劣っているとかではなく、それぞれが互いに補完しあい、欠けている観点や実用性等が相互に改善されていく事がこのブログの望むものです。
最後に、本ブログに関して意見・誤り訂正等がある場合にはご連絡頂けると助かります。

謝辞

本ブログは、メルカリでのセキュリティインターンの成果物としてセキュリティエンジニアリングチームの助力のもと執筆されました。実際の運用の視点からも多くの意見を頂いた社内CI/CDチームの皆さんにも協力頂きました。末澤裕希(@rung)さんには多角的な視点から様々な知見を与えて頂きました。株式会社Flatt Securityの米内貴志(@lmt_swallow)さんにはサプライチェーンセキュリティ全体に関する有意義な議論をさせて頂きました。重ねて感謝します。
なお本文中に誤りがあった場合でも、ここに挙げた方々の文責は一切ないことを断っておきます。

References

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加