はじめに
CI/CD のサプライチェーンセキュリティは、これまで「悪性の入力を入れない」ことを中心に設計されてきた。しかし、未知のペイロード、正規経路を悪用した配布、実行時に初めて確定する挙動まで考えると、入口だけを検査するモデルでは限界がある。
そこで本稿では、CI/CD を予防・防御・検知・対応の 4 層に分け、特にランナー上で実際に何が起きたかを観測するランタイム観測に焦点を当てる。最後に Kubernetes 上の GitLab Runner と cicd-sensor を組み合わせた PoC を紹介する。
以前の記事「AIコーディング時代における、ソフトウェアサプライチェーン攻撃に対する防衛術」では、開発フロー全体を対象にサプライチェーン攻撃への予防策を整理した。本稿はその続編にあたる。
AIコーディングによって攻撃手法そのものが新しくなるというより、CI/CD に投入される入力と自動実行の量が増えることで、既存のサプライチェーンリスクが顕在化しやすくなっている。AI 固有の攻撃を扱うのではなく、AIコーディングによって入力と自動実行の量が増えた CI/CD を、従来のサプライチェーンセキュリティの延長として扱う。
こんな方におすすめ
- GitHub Actions / GitLab CI/CD を業務で使うエンジニア
- サプライチェーン攻撃を「予防だけ」でなく「検知」まで含めて構造的に捉えたい方
CI/CD 環境の全体像
攻撃経路について考える前に、CI/CD で何が行われているか、どこに何があるか、どのような構造的性質を持つかを整理する。
何が行われているか
CI/CD は、様々な信頼度の入力(自リポジトリのソースコード、外部依存パッケージ、ベースイメージ、サードパーティ action、シークレット)を組み合わせて、信頼が要求される成果物(ビルド済みバイナリ、コンテナイメージ、デプロイ先の状態)をビルド・配布する。ここで前提となる各入力の信頼度は、次のとおり必ずしも自明ではない。
- 自リポジトリのソースコード:多くは信頼できるが、外部 PR やメンテナ侵害で信頼できない状態になりうる
- 外部依存(パッケージ、action、ベースイメージ):もともと信頼できない
- シークレット(環境変数、OIDC 等):機密性が保証されていても、実行時に扱われる瞬間に漏洩リスクを持つ
「入り口を絞れば出口が守れる」という前提が成り立つのは、入力が全て信頼できて、変換過程で新たに信頼できない要素が入らない場合に限る。実際にはこの前提はどちらも成立しない。
構成要素とデータの流れは次のとおり。
構造的性質
ジョブが動く実行環境は次のような機密を扱う。攻撃者がジョブに侵入した場合、これらが奪取対象になる。以下は典型例で、実際の寿命や配置は設定次第で変わる。
| 機密 | 用途 | 典型的な寿命 |
|---|---|---|
| GITHUB_TOKEN / CI_JOB_TOKEN | リポジトリ操作、パッケージ公開 | 通常はジョブ単位の短寿命 |
| OIDC id-token | クラウド側ロールの短寿命引き受け | 短寿命(設定・IdP 依存) |
| 環境変数シークレット | サードパーティ API キー(Slack, npm publish 等) | 長寿命が残存しやすい |
~/.docker/config.json |
レジストリ認証情報が残る場合がある | 設定依存 |
~/.aws/credentials, ~/.kube/config
|
クラウド / Kubernetes 認証情報が残る場合がある | 設定依存 |
セキュリティを守りづらくしている構造的要因を並べる。次節の攻撃経路はこの性質から生じる。
- 多種の認証情報がパイプライン全体で扱われる:ビルド / テスト / デプロイ等のジョブがそれぞれ役割に必要な認証情報(レジストリへのプッシュ、クラウドへのデプロイ、パッケージ公開等)を持つ
- 信頼できない入力への必然的な接触:依存パッケージ・ベースイメージ・action は外部から取り込む
- 短命 + 大量並列:1 パイプラインで数十ジョブが並列に走り、秒〜分で終わる
- 使い捨ての実行環境:ジョブ終了と同時に実行環境が破棄される
- 自動化された連鎖トリガー:マージ → ビルド → デプロイが人手を介さずに動く
攻撃経路:CI/CD のどこが狙われるか
CI/CD を起点として観測するため、攻撃者の行動を「供給元 → ランナー → 外部通信 → 横展開」の 4 つの段階と、それらに横断的に働く「防御回避」に抽象化する。
攻撃を説明するために 4 段階に分解しているが、実際の攻撃では段階間を往復したり、複数段階を並列に進めたりする(keyv のようなワーム型では特にそうで、奪ったアカウントを使って別の 1 に戻るループがある)。段ごとに必要な観測点は異なる。防御回避は攻撃段階と独立の横断的な行動で、各段の発覚を遅らせる。
1. 供給元への侵入
ジョブが取り込む信頼できない入力(依存パッケージ、ベースイメージ、サードパーティ action)に悪性コードを混ぜる。最近の事例として、以下のようなものがある。
- Trivy(2026-03):侵害された認証情報を使って Trivy の GitHub Actions のタグとリリースが改ざんされ、CI/CD 上で認証情報窃取コードが実行された侵害。タグ参照を使っていたリポジトリは影響を受けたが、SHA pin で参照していたリポジトリは影響を受けなかったケースも報告されており、タグ pin(可変)と SHA pin(不変)の差が顕在化した(参考:「2026年3月19日の Trivy 再侵害の概要と対応指針」)
- axios(2026-03):週間約 1 億 DL 級の主要 npm パッケージ。悪性依存
plain-crypto-jsがマニフェストに 1 行追加されただけで、postinstallによるインストール時実行を通じて開発者端末・CI/CD ランナーの両方が直接攻撃対象になり得る典型例。悪性版の露出は約 3 時間(参考:「2026年3月31日の axios 侵害の概要と対応指針」) - keyv(2026-08):週間 1 億 DL 級パッケージのメンテナ npm アカウント侵害。
preinstallフックからsetup.mjs→ Bun ランタイム → ペイロードの多段実行チェーンで AWS / GitHub / npm / Kubernetes 認証情報を窃取し、奪った npm アカウントで他パッケージへ再配布するワーム型の連鎖侵害(参考:「keyv 等 複数著名パッケージへのソフトウェアサプライチェーン攻撃の概要と対応指針」)
攻撃対象を選ばず広範に及ぶため、影響範囲が大きい。
2. ランナー内での実行と権限奪取
ジョブに取り込まれた悪性コードが実行され、前節「ジョブが実行時に扱う機密」を奪う。パイプライン全体で見るとレジストリへのプッシュ・クラウドへのデプロイ・パッケージ公開等の認証情報が扱われるため、攻撃者はどのジョブに侵入するかによって奪える認証情報が変わる。
典型的な奪取手口は以下の通り。
- 環境変数を出力してシークレットを回収
- 決まったパス(
~/.aws/credentials、~/.docker/config.json)を走査 - 奪ったトークンのスコープ次第で、ワークフロー書き換え、他リポジトリ操作、パッケージ公開などに転用されうる
- npm / パッケージ公開トークンを奪って後続の横展開に備える
3. 実行中の外部通信
奪った認証情報を外部の C2 サーバ(Command and Control、攻撃者側の指令・受信サーバ)に送信する、または実行中に追加のペイロードを取得する。
典型パターンは、ワークフローや依存パッケージが curl / wget などで実行時に外部から追加スクリプトやバイナリを取得して実行する形。取得先の URL は実行時に決まる。
送信先も、明示的な攻撃者側のドメインだけとは限らない。GitHub API 経由で攻撃者側の公開リポジトリに blob をプッシュする、Gist に貼り付ける、といった「正規サービスを踏み台にした送信」も観測される。通信先は信頼できるドメインとして見える。
さらに、C2 ドメインをブロックチェーン上の smart contract から動的取得する設計も観測されている。実行時に contract を参照して現在の C2 ドメインを取得する形で、送信先が動的に切り替わる。
4. 横展開
奪った認証情報を使って、他のパイプライン、他のパッケージ、他のインフラへ被害を広げる。keyv 侵害では、奪った npm publish トークンを使って連鎖侵害が起き、数百パッケージに波及した。攻撃チェーンで得た成果(認証情報・アクセス)を次の攻撃対象で再利用するワーム的な挙動になる。
防御回避(横断)
上記 1〜4 の各段で並行して働く。発覚を遅らせ、事後解析のための情報を残さないことが目的。
-
set +x等でコマンドログを抑制(正当なシークレット保護でも使われるため、単独では悪性とは判断できない) - 出力を
> /dev/null 2>&1に捨てる - 難読化されたペイロードを実行時に復号
- CI 環境判定 / ロケール判定で「見られている環境」では発火しない
- 使い捨ての実行環境が破棄される前に痕跡を残さない
これにより、ジョブが「成功」で終わっても内部で何をしたかは残らず、実行環境が消えた後には解析対象となる情報自体が残らない。
対応策の全体像
CI/CD の攻撃経路への対策は、次の 4 段に分けて捉える。
- 予防:攻撃が起こる前に阻止する(例:悪性パッケージ導入前遮断、SBOM 検査、依存 pin)
- 防御:攻撃が起こっても被害を最小化する(例:最小権限、外部通信制限、hardened image、成果物経由の認証情報分離、ランナー隔離)
- 検知:攻撃の兆候・実行中の異常挙動を発見する(例:eBPF による観測 → 分析 → アラート、外部通信でのネットワーク検知)
- 対応:検知した後にアクションを取る(例:パイプライン停止、認証情報ローテーション、影響範囲の特定)。本稿では検知を中心に扱い、対応は検知後の運用として概要のみ扱う
以前の記事の予防 A〜G は、この 4 段のうち「予防」と「防御」を扱っている。それでも塞ぎきれない領域として、事前に悪性と判定できないペイロードや、正規経路を経由した悪性配布がある。これらを扱うのが「検知」以降の層である。
ジョブ実行環境と、その入出力(外部依存の取得、シークレットの注入、外部通信)に対して、予防・防御・検知の各段がどこに作用するかを示す。
対応(4 段目)は、検知でアラートが上がった後の運用側の動きで、パイプライン停止・認証情報ローテーション・影響範囲特定などを含む。ツールで完結する話ではないため、本図には含めていない。
以下、予防・防御をまとめて扱ってから、検知の中身を順に見る。
予防と防御:事前阻止と被害の最小化
CI/CD への攻撃をどう防ぐか。以前の記事で提示した予防策 A〜G は、本稿の分類で言えば「予防」と「防御」の両方を扱っている。CI/CD に絞って本記事内で要約する。
予防策 A〜G の整理
以前の記事の予防 A〜G は「入り口を絞る」ための観点整理で、実装可能な打ち手が具体的に紐づく。各予防が上の攻撃 4 段のどこに効くかを合わせて示す。
| 予防 | 対象 | 主な打ち手 | 効く攻撃段 |
|---|---|---|---|
| A:権限最小化 | 開発者権限 | 個人アカウントの権限を絞る | 1 |
| B:平文排除 | 認証情報保管 | シークレットを平文で置かない、Vault 使用 | 2 |
| C:悪性パッケージ遮断 | パッケージ導入 | プロキシで悪性検査、lockfile pin | 1 |
| D:シークレット管理 | 個人認証情報 | 短寿命化、必要時のみ払い出し | 2 |
| E:ワークフロー安全化 | CI 定義 |
permissions: 最小化、SHA pin、OIDC、成果物経由の認証情報分離 |
1, 2 |
| F:ランナー保護 | 実行環境 | 隔離、外部通信制限、hardened image | 2, 3 |
| G:公開管理 | 配布物 | 署名、SBOM、SLSA provenance | 1 |
A〜G は「予防」と「防御」の両方にまたがる。ここでは便宜上、侵入前の阻止を予防、侵入後の権限・到達範囲を限定するものを防御として整理する(E の SHA pin や F の外部通信制限のように、実際にはどちらの側面も持つ打ち手が多い)。
用語を以下で補足する。
- 成果物経由の認証情報分離:パイプラインをビルド / テスト / デプロイ等のジョブに分け、各ジョブに必要な認証情報だけを渡し、成果物だけを次のジョブに引き渡す。単一のランナー実行環境に全認証情報が集約されるのを避け、攻撃者が 1 ジョブに侵入したときに奪える認証情報の範囲を絞る
- hardened image:不要なパッケージ・権限・ツール・既知脆弱性を減らし、実行環境の攻撃面を小さくしたコンテナイメージ。distroless 系(シェル・パッケージマネージャ・汎用 CLI を含まない構成)は攻撃面の削減、上流パッチへの継続追従は残存脆弱性の削減、をそれぞれ扱う(distroless だけでは後者は保証されない)。両方を組み合わせた例に Chainguard Images や Takumi Images がある
- SBOM(Software Bill of Materials):成果物に含まれる依存物の一覧。ビルド時点の構成の宣言として作られる
- SLSA provenance:SLSA(Supply-chain Levels for Software Artifacts)は Google 発のサプライチェーン整合性フレームワーク。SLSA provenance は成果物がどの builder により、どの入力・手順から生成されたかを表す provenance metadata。provenance 自体の改ざん耐性・真正性は署名等の仕組みと組み合わせて担保する(provenance と signature は別概念)。したがって provenance を検証する側は builder 自体を trust base として評価する必要がある
- SCA(Software Composition Analysis):依存物に対する既知脆弱性・ライセンス検査。SBOM を入力として使うことが多い
予防・防御でも届かない領域
A〜G を実装すれば攻撃段 1〜3 の相当部分をカバーできる。ただし、事前に悪性と判定できない挙動を完全には排除できないため、そこには実行時の観測が要る。次に、なぜ「事前判定」に限界があるかを 4 つの理由で示す。
-
実行時に確定する挙動は静的解析で評価できない:依存パッケージが
exec/eval相当の動的評価を持ち、中身を実行時に環境変数や外部から復号する構造だと、ビルド時点では中身が空。動的評価・実行時ペイロード取得(curl | sh等)・難読化はシグナルとしては検出できても、中身までは検証できない。SBOM / SCA は依存物一覧と既知悪性検査を目的とするため、実行時に取得・生成されるペイロードはそもそも対象外 -
新規ペイロードは判定データベースに載っていない:悪性判定データベースは既に発見されたペイロードしか載せられず、公開直後はデータベースに載る前に配布される。時間的な緩衝帯として
min-release-age方針(公開後 N 分間は導入をブロック。uv / pip の設定にも、Takumi Guard などのプロキシにも実装。keyv 侵害では公開から約 2 分でブロック実績あり)があるが、悪性判定そのものではなく、緩衝時間内に判定が回らなければ通過する -
正規経路を経由した悪性配布:メンテナトークン / npm publish トークンを奪取した攻撃者は、正規の CI/CD パイプラインから OIDC provenance 付きで悪性版を公開できる。provenance は「どの builder で、どの source・input・手順から生成されたか」を示す証拠であり、builder や入力そのものが善良であることを保証するものではない。keyv 侵害では、正規の GitHub Actions を経由した公開に provenance が付いた状態でも悪性リリースになり得ることが示された
-
条件発火は静的には見えない:npm の
postinstallスクリプトや pypi のsetup.py等が、CI 環境かどうか(GITHUB_ACTIONS等の環境変数)や特定ロケール、稼働時刻を判定してから発火するペイロードは、開発者ローカルの手動確認では起動しない。post-installの悪性判定は悪性判定データベースに載っていれば可能だが、新規ペイロードは防御を通過する
予防・防御のカバレッジと残る領域を、攻撃 4 段ごとに整理する。
| 攻撃段 | 予防・防御でのカバー | 残る領域 |
|---|---|---|
| 1 供給元への侵入 | ○ 大部分(C, G) | 新規ペイロード、min-release-age 遅延中、正規経路悪用 |
| 2 実行と権限奪取 | ○ 大部分(B, D, E) | 実行時に発火してから奪取される場合 |
| 3 実行中の外部通信 | △ 一部(F の外部通信制限) | 動的 C2、正規ドメイン経由の送信 |
| 4 横展開 | △ 一部(認証情報最小化で範囲を絞る) | 奪取済み認証情報の再利用 |
これらの「残る領域」は共通して、事前判定できない挙動が実行された後にしか正体を現わさない。ここに検知の層が要る。
検知:観測から検知・対応への連鎖
検知は 1 段の話ではない
「検知」と一括りにされることが多いが、実際には次のような連鎖になっている。
- 観測(Observation):ジョブや通信の raw イベント(システムコール、外部通信、ファイルアクセス、プロセスツリーなど)を取得する
- 分析(Analysis):raw イベントに対して、ベースラインとの差分、ルール、機械学習等で悪性の可能性を評価する
- 検知(Detection):分析結果からアラートを発火し、注意を要する事象として通知する
- 対応(Response):アラートを受けてパイプライン / ランナーの隔離 → 認証情報のリボーク・ローテーション → 成果物 / パッケージ / リポジトリの影響範囲調査 → 再ビルド・再デプロイ、の順で運用側が動く。CI/CD では「検知したのに、そのジョブが最後まで走る」問題が起きやすく、kill / quarantine の位置付けを最初に決めておく必要がある
本稿で扱う eBPF は、この 4 段のうち「観測」に位置する。eBPF 自体はルール評価・ポリシー enforcement・block/kill まで担える技術で、eBPF ベースのセキュリティ製品には検知・遮断まで実装するものもある。
cicd-sensor 自身も、raw イベントの収集だけでなくベースライン検知と、検知時にプロセスを kill してジョブを停止する部分的なネットワークブロック(ファイアウォールのようにトラフィックをフィルタするのではなく、プロセス側を止める形)まで提供している。
cicd-sensor が具体的にどの機能を提供しているかは cicd-sensor 公式ドキュメント を参照。
実行環境の内側と外側、どちらから観測するか
実行中の挙動を観測する層は、大きく 2 通りある。
- 実行環境の内側から:ジョブが動くノード上で、システムコール / プロセス / ファイルアクセス / ネットワーク I/O を捕捉する。eBPF が代表的な実装手段
- 実行環境の外側から:ジョブから外部への通信を、ジョブが動く境界の外(egress ゲートウェイ、DNS リゾルバ、NDR(Network Detection and Response、ネットワーク挙動の監視)等)で観測する。ジョブが動く環境のネットワーク構成に依存する
両者は競合ではなく相補する。内側観測はジョブ内部の細かな挙動が見えるが、ノードに手が入る環境でしか使えない。外側観測は「外部への通信」に限定されるが、ジョブに手を入れずに実現でき、既存のネットワークインフラで実現できる場合もある。
内側からの観測:eBPF ベース
eBPF(extended Berkeley Packet Filter)は、Linux カーネル内で動作する小さなプログラムを、既存のカーネルコードを書き換えずに注入する仕組みである。カーネルの各種フック(tracepoint / kprobe / LSM / socket など)に紐付けて、プロセスの実行、システムコール、ファイル I/O、ネットワーク I/O といったランタイムイベントを低オーバーヘッドで取得できる。ユーザ空間のプロセスに手を入れずに、実行中の挙動を外側から観測できるのが最大の特徴。eBPF プログラムはロード時にカーネル内 verifier で安全性が静的に検証されるため、本番環境で運用されている。
CI/CD 文脈での効用は次の 3 点にある。ノード側から観測するため、ジョブ本体のスクリプトに観測処理を追加する必要が無い。難読化されたペイロードであっても、実行時に発生するシステムコール / 外部通信 / ファイルアクセスは raw イベントとして捕捉できる(eBPF は「ペイロードの中身が何か」を理解するのではなく、「実行に伴ってカーネル上で何が起きたか」を観測する。raw イベントから悪性を識別するのは後段の分析の課題であり、eBPF 単体で完結しない)。
観測結果はノードの Agent 経由でリアルタイムに外部に送信されるため、ジョブ終了時の実行環境破棄によって観測データまで失われる問題を避けられる(Agent 自身の侵害、ノード破壊、Agent → Manager 間の送信失敗などの例外はある)。
観測できる粒度は、プロセス / ファイル / ネットワーク / 認証情報アクセスに及ぶ。具体的には、システムコール(プロセス起動の execve、ファイル open の openat、ネットワーク接続の connect、ファイル削除の unlink、他プロセスの trace/操作を行う ptrace などの発生)、外部通信(通信先 IP / ソケット情報、DNS クエリ等)、ファイルアクセス(どのプロセスがどのパスに読み込み / 書き込みしたか、認証情報ファイルへのアクセス等)、プロセスツリー(親子関係、実行バイナリのパス)などが取得できる。
SBOM / SCA との対比
eBPF 観測(さらにその上の分析・検知)と、既存の SBOM / SCA は相補関係にある。
| 観点 | SBOM / SCA | ランタイム観測 |
|---|---|---|
| 対象 | 静的な依存関係・成果物 | 実行時の挙動 |
| 主な検出 | 既知 CVE、既知悪性依存 | プロセス / システムコール / ファイル / ネットワーク |
| 苦手なもの | 実行時取得、条件発火、未知ペイロード | 静的な脆弱性・未実行コード |
| タイミング | ビルド / マージ前後 | ジョブ実行中 |
予防・防御層が捕まえられる範囲は SBOM / SCA が担い、それを抜けたものの raw イベントを eBPF 観測が集め、その上の分析で悪性を識別する。
観測から検知への現実的な課題
raw イベントを取得できることと、そこから攻撃を検知できることは別問題である。正常な CI ジョブでも大量の execve / openat / connect が発生する。悪性挙動を大量の正常イベントの中から識別するには、ベースラインの学習、ルールの整備、正常ジョブとの差分検出などが要る。ここは分析側の設計と運用の話であり、観測ツールを入れただけで自動的に解けるわけではない。
この現実を踏まえた上で、観測層をまず整えることが、後段の分析・検知・対応を回すための前提になる。
cicd-sensor の構造
eBPF ベースの汎用ランタイム観測ツールは既に存在するが、CI/CD 環境で使う場合には特有の要件がある。ジョブは秒〜分単位で終わるため、開始直後から観測を始めないと初期イベントを取り損なう。1 パイプラインで数十ジョブが並列に走るため、ジョブごとにコンテキストを分離する必要がある。ジョブ実行環境は使い捨てで、実行環境が破棄される前に観測結果を外部へ回収しておく必要がある。
これらを織り込んで設計された CI/CD 特化の OSS として cicd-sensor がある(本稿で扱う Kubernetes Runner連携は執筆時点で preview support)。ここでは Kubernetes 上で Self-managed Runnerを回す構成を例に、Agent と Manager の 2 段構造を示す。以下は本稿執筆時点の cicd-sensor(Kubernetes Runner preview)の構成の概念図。
図中に登場する仕組みを補足する。
- containerd:Kubernetes ノード上で Pod / Container の生成・破棄を担うコンテナランタイム
- NRI(Node Resource Interface):containerd 側の plugin インタフェース。Pod のライフサイクルイベントを外部 plugin に通知できる
- cgroup(control group):Linux のリソース隔離単位。Pod / Container ごとに割り当てられる
流れは次のとおり。
- containerd がコンテナのライフサイクルイベント(
CreateContainer等)を NRI plugin 経由で Agent に通知する - Agent はその情報から対象コンテナ / ジョブの cgroup を特定し、その cgroup を観測対象として eBPF で追跡する
- ジョブ Pod 内で発生したシステムコール / ネットワーク / ファイルアクセスが、カーネル空間の eBPF program で捕捉される
- eBPF program は捕捉したイベントを Agent のユーザ空間側に渡す
- Agent は複数 Pod のイベントを集約し、Manager に送信する
Agent はノード常駐の DaemonSet として動作し、1 ノードに 1 インスタンスが稼働してそのノードの全ジョブ Pod を観測する。Manager は複数ノードにまたがる Agent の出力を 1 箇所で受ける集約層として動作する。
Manager の運用と分析をどこに置くか
観測(Agent)と分析・検知(Manager 以降)は別のレイヤであり、必要な専門性も違う。Manager 以降の置き場所は次の 2 通りに分かれる。
- 自組織で運用:Manager を自組織の Kubernetes に立てて、分析基盤・検知アラートの整備も自組織で行う。ログの保管・可視化・脅威判定を組織内で完結できる反面、分析ルール整備と運用の負荷を組織側が持つ
- マネージドサービスに委ねる:Takumi Runner の cicd-sensor 連携 では、cicd-sensor Agent から Shisho Cloud 側の Manager にトレースを送信し、分析・脅威検出をセキュリティ会社の分析基盤に委ねられる
Manager 以降を外部のセキュリティ専門会社に委ねる価値は、次の 3 点にある。
- 分析ルールを最新の攻撃動向に基づいて継続的に改善する動きが、日常的に観測・対応を行っている専門会社側に集約される
- たとえ即時に検知できなかった場合でも、観測データが外部に保管されるので、事後に影響範囲を追跡し遡及的に対応を検討できる
- 検知ルールを更新するには CI 環境で悪性に見える挙動を再現して検証する必要があり、実装・検証の継続コストが高い。加えて、ルールを厳しく組みすぎると誤検知でリリースが不必要にブロックされる副作用があり、このバランスを取りながら継続的に検証するのが困難
選択の分岐点は 2 つある。ひとつは、自組織で eBPF 観測ログを分析できる体制があるかどうか。もうひとつは、CI 実行時の観測データ(コードパス、認証情報の使用パターンなど、機微になりうる情報を含む)を外部に提供できるかどうか。前者は運用能力、後者はデータ取扱いポリシーの話で、両方のバランスで置き場所が決まる。
外側からの観測:ネットワーク層
ノード側に手を入れられない環境では eBPF による内側観測は使えない。この場合、あるいは内側観測と並行して、ジョブから外部への通信をジョブが動く境界の外で観測する層を置く。egress ゲートウェイやプロキシで送信先を検査する、DNS リゾルバでクエリと応答をログ化してブロックリストと照合する、NDR や IDS(Intrusion Detection System、侵入検知システム)で異常な通信パターンを検出する、といった手段がある。
ただし現代的な C2 は正規サービス(GitHub API、Gist、smart contract 等)を踏み台にするため、DNS / ドメインレピュテーションだけでは不十分で、宛先、SNI、HTTP メタデータ、IP、ASN、TLS、通信頻度など複数のシグナルを組み合わせる必要がある。
ジョブが動く環境が VPC / ファイアウォールを持つ場合、既存のネットワークインフラに乗せて実現できる。逆に自組織でランナーノードやネットワーク境界を制御できない場合、ジョブ内部・外部の通信を自組織側で直接観測することは難しくなる。この場合、パイプライン前段(依存パッケージのミラー、成果物保管の外部通信)で見る形が実務的な着地点になる。
外側観測は「ジョブから外に何が出て行ったか」の集約を担う。内側観測(eBPF)と組み合わせることで、実行環境内部の細かな挙動と外部通信の両方を可視化できる。ここでも「観測が取れる ≠ 攻撃を検知できる」の関係は同じで、raw イベントから悪性を識別する分析の設計が別途要る。
参照実装
予防・防御・観測の各要素(hardened image / Self-managed Runner / cicd-sensor / 観測基盤)は、単体では紹介記事や公式ドキュメントで参照できる。実際に通しで動く形を確認したい場合は、これらを組み合わせて動作を追う必要がある。
その組み合わせを単一の Mac 上で通しで動かせる PoC を公開している。
- Linux VM に GitLab Community Edition + Container Registry + Kubernetes(kubeadm でセットアップ)+ GitLab Runner を通しで構築
- hardened distroless(Chainguard Images)でジョブ Pod を回す
- cicd-sensor の Agent(DaemonSet)と Manager を Kubernetes に展開
- Grafana で eBPF で取得されたログをモニタリング
おわりに
本稿では、CI/CD 環境のサプライチェーン攻撃を「全体像 → 攻撃経路 → 対応策」の 3 段で構造化し、対応策を予防・防御・検知・対応の 4 段で捉え直した。予防と防御は既存の予防策 A〜G が扱う範囲。それらでも塞ぎきれない「事前判定できないケース」を、実行後の観測・分析・検知で拾うのが本稿の主題である。
観測層としての eBPF は Kubernetes 環境での挙動観測手段として広く使われるようになっているが、raw イベントを取得できることと攻撃を検知できることの間には、分析・検知アラート・対応の運用という別の層がある。ここを自組織で整えるか、マネージドサービスに委ねるかは、組織の体制次第となる。
CI/CD のサプライチェーンセキュリティを「悪性入力を入れない」問題だけとして扱うと、未知ペイロード、正規経路を悪用した配布、実行時に決まる挙動を扱えない。重要なのは、入力を信頼できるかどうかだけでなく、信頼境界を越えた後に何が実行されたかを観測できることである。SCA・provenance・認証情報最小化・外部通信制限といった予防・防御に加えて、ランナーのランタイム観測を組み合わせる必要がある。
自組織の CI/CD 環境について、予防・防御・検知・対応の各段をどこまで持っているか、残る領域はどこかを整理することが、次の一手を決める起点になる。
参考