なぜCIなのか
サプライチェーンセキュリティをCIで行うべき理由
出荷するソフトウェアとは、CIのビルドがその時点でダウンロードしたものから生成した成果物にほかなりません。何が取り込まれたかを確認し、何を取り込んでよいかを決め、リリースに何が含まれたかを記録できる唯一の場所が、そのビルドです。
更新日
What software supply chain security means
講演での実用的な定義です。ソフトウェアとは自社のコードとそれが依存するすべてのものであり、サプライチェーンとはそれらの部品をリリースに組み込むすべての工程です。
Kubernetes Community Days Helsinki 2026でのPetteri Pulkkinen、Erika Marttinen、SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chainsの2:35~4:28。
本番のビルドは、手元のPCのビルドとは違う
多くのチームは、ローカルでテストしたコードがそのまま出荷されると考えています。ソースは同じでも、ソフトウェアは同じでないことがよくあります。ビルドとは、ソースコードに加えて、実行時に取得するものと、その時点の周辺環境のすべてを合わせたものであり、そのほとんどは固定されていません。
ピン留めされていない依存関係は、解決結果が変わる
依存関係のバージョン範囲指定はごく一般的です。npmでは^1.0.4が以降の1.xリリースをすべて受け入れるため、同じpackage.jsonでも日によって異なるバージョンがインストールされます。ロックファイルはバージョンを固定しますが、それはロックファイルがコミットされ、ビルドがそれに従う場合に限られます。npm ciはロックファイルとpackage.jsonが一致しないと失敗しますが、npm installはロックファイルを黙って更新することがあります。そもそもロックファイルをコミットしていないプロジェクトも数多くあります。広く使われているWebフレームワークのExpress 5.2.1もその1つです。
ピン留めしたバージョンでさえ、ファイルそのものではなく名前にすぎません。pipのドキュメントでは、バージョンのピン留めだけでは、パッケージインデックスが同じバージョンに対して異なるファイルを配信するのを防げないと説明されており、そのためにハッシュチェックモードが用意されています。
時間がインプットを変える
レジストリはビルドとビルドの間に変化します。新しいバージョンが公開され、古いバージョンは取り下げられ、月曜日には安全に解決されたバージョン範囲が、火曜日には侵害されたリリースを取り込むこともあります。コンテナのタグも動きます。Docker自身のドキュメントが「イメージタグは変更可能であり、公開者はタグを新しいイメージを指すように更新できる」と警告しているとおり、ダイジェストでピン留めしない限り、FROM node:20は月が変われば同じベースイメージではありません。
時間は出力の中にも入り込みます。Reproducible Buildsプロジェクトは、多くのツールが現在の日時をビルド成果物に記録するため、タイムスタンプを「再現性の問題の最大の原因」と呼んでいます。
環境が異なる
- OSとアーキテクチャ。macOSとARMのノートPCと、Linuxとx86-64のCIランナーでは、同じパッケージでもインストールされるビルド済みバイナリが異なります。現在、多くのnpmツールは、コンパイル済みバイナリをラップしたプラットフォーム別のパッケージとして提供されています。
- ツールチェーン。コンパイラ、ランタイム、パッケージマネージャーのバージョンは、すべてをピン留めしない限り、マシン間でずれていきます。
- キャッシュ。手元のマシンとCIは、異なる時点に異なる解決結果から作られた、異なるキャッシュを復元します。
- インストールスクリプトとビルドステップ。ポストインストールフック、セットアップ用のアクション、ビルドスクリプトは、ビルド時にツールやデータをダウンロードします。それらはいずれもマニフェストには現れません。
- 設定。環境変数、ビルドフラグ、シークレットはCIにしか存在しません。
Reproducible Buildsプロジェクトは、再現可能なビルドを「同じソースコード、ビルド環境、ビルド手順が与えられれば、誰でも指定されたすべての成果物のビット単位で同一のコピーを再作成できる」ビルドと定義しています。この定義から、どれだけ多くのものを一定に保たなければならないかがわかります。ほとんどのチームのビルドはそこまで到達しておらず、到達したチームは多大な努力を払っています。そこに至るまでは、何が出荷されたかを示す唯一の信頼できる記録は、出荷されたビルドの実行中に記録されたものだけです。
A software supply chain, walked end to end
Kubernetes Community Days Helsinki 2026でのPetteri Pulkkinen、Erika Marttinen、SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chainsの15:20~16:38。
チェックできる場所と、それぞれの場所から見えるもの
セキュリティチェックを実行できる場所は4つあります。それぞれに役割がありますが、実際にリリースを生成したビルドを見られるのは、そのうちの1つだけです。
| 機能 | CIのビルド内 | 開発者のマシン | リポジトリのスキャン | 成果物のスキャン |
|---|---|---|---|---|
| 本番が実際に取得したバージョンを把握 | 取得時に記録 | 別のインストール | ロックファイルで固定されている場合のみ | 認識できるもののみ |
| 推移的依存関係とキャッシュから復元されたパッケージを把握 | CIではなく自身のキャッシュ | ロックファイルのみ | ||
| インストールスクリプトがダウンロードするものを把握 | ||||
| 悪意ある外向き通信を阻止 | ランナーでのエグレスポリシー | |||
| コードのプッシュ前に問題を検出 | プルリクエスト時 |
- 対応
- 一部対応
- 非対応
リポジトリのスキャンが報告するのは、プロジェクトが宣言している内容です。完成した成果物のスキャンが報告するのは、その中で認識できたものです。どちらもビルドが行われる様子を見てはいません。Express 5.2.1はロックファイルをコミットしていないため、クリーンなチェックアウトに対してスキャナーを実行しても、解決すべきnpmの依存関係が何もなく、それ以外に認識できたものを報告するだけです。同じツールを同じリポジトリのインストール済みのコピーに向けると、カタロガーのフラグを1つ変えただけで、結果はまた変わります。返ってくる内容はスキャン前に何がインストールされていたかに左右され、それこそがスキャンでは制御できない部分です。スキャンの結果が食い違う理由と、ビルドを記録することで何が変わるかは、ビルド時SBOMのページで解説しています。
パイプラインそのものが攻撃対象になる
CIはソースコード、依存関係、シークレットが集まる場所であり、それゆえに攻撃する価値があります。
- tj-actions/changed-files(2025年)。広く使われているGitHub Actionが侵害され、CIのシークレットがビルドログに出力されました。
- Codecov Bash Uploader(2021年)。顧客のCIで実行される改ざんされたスクリプトが、環境変数を攻撃者のサーバーに送信しました。
- Shai-Hulud npmワーム(2025年)。侵害されたパッケージがインストール時に認証情報を盗み、それを使ってさらに侵害されたパッケージを公開しました。
- xz utils(2024年)。バックドアは、目に見えるソースではなく、リリースのtarballとビルドプロセスそのものに隠されていました。
いずれも、ビルドやインストールの最中にコードを実行するものでした。その前に実行したリポジトリのスキャンや、その後に実行した成果物のスキャンでは、ビルドプロセスが何をしたかは見えなかったでしょう。ジョブがアクセスできる範囲を監視し制限する管理策は、ジョブが実行される場所に置く必要があります。これは社内向けや目立たないアプリにも当てはまります。こうした攻撃が狙うのは、アプリの機能ではなく、パイプラインと、あらゆるアプリが共有するパッケージだからです。社内向け・低リスクのソフトウェアのためのサプライチェーンセキュリティをご覧ください。
コードと依存関係は増え、それを読む人は減っている
AIコーディングエージェントは、チームがすべての行をレビューできる速さを超えてコードを書き、変更し、その過程で依存関係を追加します。存在しないパッケージ名を挙げることもあります。USENIX Security 2025で発表された、生成された576,000件のコードサンプルを対象とする研究では、ハルシネーションによるパッケージの割合が、商用モデルで少なくとも5.2%、オープンソースモデルで21.7%に上りました。そうした名前を登録した攻撃者のパッケージは、その名前を信頼した次のビルドでインストールされてしまいます。
誰が書いたものであれ、何が書いたものであれ、すべての変更は本番に向かう途中でビルドを通過します。そのため、ビルドは誰かが目を通したかどうかに依存しない唯一のチェックポイントになります。
エビデンスは、出荷したものを記述していなければならない
規制や顧客が求めているのは、意図ではなく証明です。EUのCyber Resilience Actは、メーカーに対し、製品に含まれるコンポーネントを特定・文書化し、製品のサポート期間中、それらの脆弱性に対処することを求めています。マニフェストから生成したSBOMが記述するのは、プロジェクトが要求したものです。ビルドから記録したSBOMが記述するのは、お客様が責任を負うリリースに実際に含まれたものです。新しい脆弱性が公開されたとき、どのリリースが影響を受けるかを教えてくれるのは後者です。CRAの要件をご覧ください。
「CIで行う」が意味すべきこと
CIのステップとしてスキャナーを実行するのは第一歩ですが、それでもファイルが届いた後に見ていることに変わりはありません。CIで適切に行うとは、次のことを意味します。
- 推測せず、記録する。推移的依存関係やキャッシュから復元されたものも含め、ジョブが実際に取得するすべてのパッケージとダウンロードを捕捉します。
- 記録の完全性を明示する。SBOMは、見つかったものを列挙するだけでなく、ジョブが取得したすべてをカバーしているかどうかを示すべきです。
- エグレスを制御する。ジョブがアクセスできるホストを決め、侵害された依存関係が外部と通信する前に、それ以外をブロックします。
- リリース後も監視を続ける。新しい脆弱性が現れるたびに出荷済みのものを再確認し、検出結果を修正につなげます。
これがCRACIの役割です。CRACIはGitHub Actionsのジョブのためのランナーであり、各ビルドが取り込むものを記録し、エグレスポリシーを適用し、出荷したものを監視します。ワークフローの移行は1行で済みます:runs-on: craci。CRACIの仕組みをご覧ください。
CIでは置き換えられないもの
サプライチェーンセキュリティをビルドに組み込んでも、他のレイヤーが不要になるわけではありません。
- 開発者のマシンは、依然として認証情報を保持し、インストールスクリプトを実行します。こちらも保護してください。侵害されたノートPCは、ビルドが実行される前にコードをプッシュできてしまいます。
- コードレビューと静的解析は、依存関係の記録では見つからない、自社コードの欠陥を見つけます。
- ピン留め、ロックファイル、ダイジェストは、引き続き有効なプラクティスです。ビルド間の差を縮めてくれます。それでも、ビルドを記録すれば実際に何が起きたかがわかります。
チェックすべき場所はビルドだけではありません。しかし、実際に出荷するソフトウェアを見られる唯一の場所はビルドです。