CyberFix Note
セキュアコーディング

CI/CDパイプラインのセキュリティ対策、GitHub ActionsとGitLab CIで秘密情報とRunnerを守る設計

対象の目安: CI/CDを設計する開発者 / プラットフォームエンジニア / 実務

ソウ・攻撃・脆弱性リサーチ担当
・ 約21分で読めます
CI/CDパイプラインのセキュリティ対策、GitHub ActionsとGitLab CIで秘密情報とRunnerを守る設計

CI/CDのパイプラインは、開発の中でも特に権限が集まる場所です。リポジトリへの書き込み、パッケージの公開、クラウドの認証情報、本番環境へのデプロイ権限が、ジョブの実行環境へ渡されます。攻撃者から見ると、パイプラインを一度乗っ取れば、本番環境やソフトウェアの利用者へ直接届く経路になります。

2025年3月には、GitHub Actionsで広く使われていたtj-actions/changed-filesのタグが書き換えられ、多数のリポジトリでCIの秘密情報がログへ書き出されました(CVE-2025-30066)。2026年10月には、GitLabのCI/CD設定の正規表現を入口に、GitLabのサーバーそのものでコードを実行されるCVE-2026-97229がCISAのKEVに載りました。この記事は、CI/CDを設計する開発者とプラットフォームエンジニアに向けて、OWASPの「CI/CDの10大リスク」を土台に、パイプラインの守り方をGitHub ActionsとGitLab CIの公式文書で整理します。

CI/CDパイプラインが狙われる理由

パイプラインのジョブは、開発者が書いたスクリプトを、組織の権限で自動的に実行する仕組みです。ジョブには次のものが渡されます。

  • リポジトリへの読み書きのトークン(GitHubのGITHUB_TOKEN、GitLabのCI_JOB_TOKEN)
  • パッケージ公開用のトークン、コンテナレジストリの認証情報
  • クラウドのアクセスキー、デプロイ先のSSH鍵
  • 署名鍵や、本番のデータベースへの接続情報

ジョブで動くコードを攻撃者が少しでも操作できれば、これらをまとめて持ち出せます。しかも、パイプラインの実行は正規の動作として記録されるため、通常の侵入より発見が遅れがちです。tj-actions/changed-filesの事件では、NVDの記載によれば、v1からv45.0.7までのタグが2025年3月14日から15日にかけて悪意あるコミットを指すように書き換えられ、そのアクションを使うワークフローのログから秘密情報を読み取れる状態になりました。CISAは3月18日にKEVへ追加しています。

OWASPのCI/CDの10大リスク

OWASPは、CI/CDの環境に特有のリスクを10項目にまとめています。項目名はOWASPの一覧のとおりで、説明はこの記事での要約です。

IDリスク要約
CICD-SEC-1Insufficient Flow Control Mechanismsレビューや承認を経ずに、コードや成果物が本番まで流れてしまう
CICD-SEC-2Inadequate Identity and Access Management人とサービスのアカウントの権限が過大で、棚卸しされていない
CICD-SEC-3Dependency Chain Abuse依存パッケージの取り違えや乗っ取りで、悪意あるコードがビルドに入る
CICD-SEC-4Poisoned Pipeline Execution (PPE)パイプラインで動くコマンドを、リポジトリへの書き込みなどを通じて攻撃者が差し込む
CICD-SEC-5Insufficient PBACパイプラインの各ジョブが、必要以上の資源や秘密情報へ届く
CICD-SEC-6Insufficient Credential Hygiene秘密情報の埋め込み、長期の認証情報、ログへの露出
CICD-SEC-7Insecure System ConfigurationCI/CDの基盤やRunnerの設定の不備、古い版のまま運用
CICD-SEC-8Ungoverned Usage of 3rd Party Services外部のサービスや連携アプリへ、管理されないまま権限を渡している
CICD-SEC-9Improper Artifact Integrity Validation成果物が改ざんされていないかを確かめずに配布やデプロイをする
CICD-SEC-10Insufficient Logging and Visibility基盤の操作やパイプラインの変更が記録されず、侵害に気づけない

この中で、パイプラインに固有の攻撃として押さえておきたいのがCICD-SEC-4のPPEです。OWASPは、パイプラインの定義ファイルを直接書き換える型、定義ファイルが呼び出すスクリプトやMakefileなどを書き換える型、公開リポジトリへの外部からのプルリクエストで仕掛ける型を挙げています。いずれも、攻撃者が「パイプラインが実行するもの」に手を入れられることが起点です。

OWASP Top 10 CI/CD Security Risksは、CICD-SEC-1のInsufficient Flow Control MechanismsからCICD-SEC-10のInsufficient Logging and Visibilityまでの10項目を挙げ、項目ごとに個別のページで攻撃の例と推奨事項を説明しています。

パイプライン定義の変更を管理する

パイプラインの定義ファイルを書き換えられる人は、そのパイプラインに渡る秘密情報と権限をすべて使えます。定義ファイルは、アプリのコード以上に変更の経路を絞ります。

  • 既定のブランチを保護ブランチにし、直接のpushを禁止してレビューを必須にする
  • GitHubでは.github/workflows/、GitLabでは.gitlab-ci.ymlと呼び出し先のスクリプトを、コードオーナー(CODEOWNERS)の対象にする
  • 本番へのデプロイを行うジョブは、保護ブランチと保護された環境(Environments)からだけ動くようにし、承認者を置く

GitHubの文書は、.github/workflowsをコードオーナーに加えると「変更の提案は、指定したレビュアーの承認を先に必要とする」と説明しています。ただし、CODEOWNERSのファイルを置くだけでは、コードオーナーにレビューが依頼されるだけです。承認を必須にするには、GitHubではブランチ保護かルールセットでコードオーナーのレビューを必須にし、GitLabでは保護ブランチの設定でコードオーナーの承認を必須にします。GitLabのコードオーナーはPremiumとUltimateの機能です。GitLabのRunnerの文書も、プロジェクトでDeveloperの役割を持つ利用者は誰でも環境の安全を損ないうると書いています。定義ファイルを書き換えられる人の範囲が、そのまま攻撃面になります。

外部からのプルリクエストのコードを権限のある文脈で動かさない

公開リポジトリでは、誰でもフォークからプルリクエストを送れます。そのコードをパイプラインで動かすときは、秘密情報や書き込み権限に届かない文脈で動かすことが前提です。

GitHub Actionsでは、フォークからのプルリクエストでpull_requestイベントのワークフローが動く場合、GITHUB_TOKENを除いて秘密情報はRunnerへ渡されず、GITHUB_TOKENも読み取り専用になります。一方のpull_request_targetは、ベースのリポジトリの文脈で動き、秘密情報と書き込み権限を持ちます。GitHubの文書は、必要がなければpull_request_targetを使わないこと、使う場合も「フォークからのものを含め、信頼できないコードを明示的にチェックアウトしてはならない」ことを求めています。権限のある処理が必要なら、権限のないpull_requestで検査し、その結果をworkflow_runで受け取って権限のある処理を行う形に分けます。分けた後も、後段のワークフローは秘密情報と書き込み権限を持つため、前段から受け取る成果物やキャッシュは信頼できない入力として扱います。後段でプルリクエストのコードをチェックアウトしたり実行したりせず、受け取るデータは用途と形式を決めて検証してから使います。

GitLab CIでは、フォークからのマージリクエストのパイプラインは、既定ではフォーク側のプロジェクトで、フォーク側の設定、資源、変数を使って動きます。親プロジェクトのメンバーが親プロジェクトでパイプラインを実行することもできますが、このときはフォークのブランチのCI設定が、親プロジェクトの変数と資源で動きます。GitLabの文書は、フォークからのマージリクエストには親プロジェクトの秘密情報を盗もうとするコードが含まれうるため、実行の前に変更を注意深く確かめるよう警告しています。保護された変数と保護されたRunnerは、フォークからのマージリクエストのパイプラインからは使えません。

注意

親プロジェクトでフォークのパイプラインを動かす操作は、マージの前であっても、フォークのCI設定に親プロジェクトの変数を渡すことになります。確認画面の警告は、レビューを終えたという意思表示として扱い、変更を読む前に実行しない運用にします。

信頼できない値をスクリプトへ直接埋め込まない

プルリクエストのタイトル、ブランチ名、Issueの本文などは、外部の人が自由に決められる値です。GitHub Actionsで、これらを${{ }}の式でrunのスクリプトへ直接埋め込むと、式が展開された後の文字列がシェルとして解釈され、値に仕込まれたコマンドが動きます。GitHubの文書は、値を中間の環境変数に入れてから参照する書き方を勧めています。

# 危険: タイトルがそのままシェルのスクリプトの一部になる
- name: Check title
  run: |
    echo "${{ github.event.pull_request.title }}"

# 推奨: 値は環境変数として渡し、シェルの変数として参照する
- name: Check title
  env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    printf '%s\n' "$TITLE"

環境変数として渡した値は、スクリプトの組み立てには使われず、シェルの変数の中身として扱われます。変数を参照するときは二重引用符で囲み、値が単語分割されないようにします。GitLab CIでも、パイプラインの変数でジョブの動作を上書きできる設計は同種の危険を持つため、GitLabの文書は型の検証ができるCI/CDの入力(inputs)を使い、パイプライン変数の無効化も検討するよう勧めています。

ジョブのトークンを最小の権限にする

ジョブに自動で渡されるトークンは、既定の権限が広いまま使われがちです。GitHub Actionsでは、ワークフローの先頭でpermissionsを読み取りに絞り、書き込みが必要なジョブだけで個別に広げます。

permissions:
  contents: read   # ワークフロー全体の既定は読み取りだけ

jobs:
  release:
    permissions:
      contents: write   # このジョブだけ書き込みを許す

GitLab CIのCI_JOB_TOKENは、ジョブを実行した利用者の権限をもとに、ほかのプロジェクトのAPIやリポジトリへ届きます。GitLabは、トークンで自分のプロジェクトへ届いてよいグループとプロジェクトを許可リストで限る設定を用意しています。プロジェクトごとに、許可する相手を必要なものだけに絞ります。公開(Public)か内部(Internal)のプロジェクトでは、公開パイプラインの成果物など一部の資源に、許可リストにないプロジェクトのトークンからも届くとGitLabの文書は説明しています。この経路も許可リストに限る設定があるため、公開か内部のプロジェクトではあわせて確認します。

秘密情報は短命にし、置き場所を分ける

CIの秘密情報は、漏れたときの被害の期間と範囲を小さくする設計にします。

  • クラウドへの認証は、長期のアクセスキーを変数に置く代わりに、OIDCで短期の認証情報を受け取る(GitHubのOIDC、GitLabのID token)
  • クラウド側の信頼設定では、受け入れる主体(sub)をリポジトリ、ブランチ、環境の単位まで絞る
  • GitLabで変数を使う場合は、マスク、非表示、保護を付け、保護ブランチと保護タグのパイプラインだけに渡す。マージリクエストのパイプラインに保護された変数を渡さない方針なら、CI/CDの設定にある「Allow merge request pipelines to access protected variables and runners」を無効にしておく
  • 最も機密性の高い秘密情報は、GitLabの変数ではなく外部の秘密情報管理の仕組みに置き、実行時に取り出す

GitLabの文書は、CI/CDの変数は秘密情報管理の製品より安全性が低く、非表示にしていない変数の値は設定を見られる利用者が読めると説明しています。GitHubの文書は、秘密情報をJSONやYAMLのような構造化データにまとめて1つの秘密情報として登録しないよう求めています。ログの自動のマスクは完全一致で働くため、まとめた値の一部は伏せられないことがあり、自動のマスクは保証されないとも明記しています。

OIDCの設定例として、GitLab CIからAWSのロールを引き受ける場合は、GitLabの文書の例に沿うと次のようになります。audには、AWSのIAMに登録したOIDCプロバイダーの対象(audience)と同じ値を入れます。assume-role-with-web-identityの応答には一時的な秘密鍵とセッショントークンが含まれるため、応答をそのまま表示せず、必要な値だけを環境変数に取り込みます。

deploy:
  stage: deploy
  environment: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.example.com
  script:
    # 応答はログへ出さず、3つの値だけを環境変数に設定する
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn "$ROLE_ARN"
      --role-session-name "gitlab-$CI_PROJECT_ID-$CI_PIPELINE_ID"
      --web-identity-token "$AWS_ID_TOKEN"
      --duration-seconds 900
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text))
    - aws sts get-caller-identity

AWS側のロールの信頼ポリシーでは、トークンのsubをproject_path:mygroup/myproject:ref_type:branch:ref:mainのように、プロジェクトとブランチまで限って受け入れます。GitHub Actionsのsubは、ジョブの条件で形が変わります。環境(environment)を指定したジョブではrepo:octo-org/octo-repo:environment:Production、プルリクエストのイベントではrepo:octo-org/octo-repo:pull_request、それ以外のブランチの実行ではrepo:octo-org/octo-repo:ref:refs/heads/mainの形です。

さらにGitHub.comでは、2026年7月15日より後に作られたリポジトリの既定のsubに、所有者とリポジトリの変更されないIDが入ります(repo:octo-org@123456/octo-repo@456789:ref:refs/heads/mainのような形)。それより前のリポジトリは、設定で切り替えない限り従来の形のままです。GitHub Enterprise Serverは対象外です。信頼ポリシーには、対象のリポジトリが実際に発行するsubの形を書きます。主体を絞らないと、同じプロバイダーを使うほかのリポジトリのジョブからもロールを引き受けられてしまいます。秘密情報の保管とローテーションの全体像は、次の記事にまとめています。

あわせて読みたい

シークレット管理の実務。APIキー・認証情報をハードコードせず、Vaultやマネージドサービスで守りローテーションする

サードパーティの部品は不変の参照で固定する

tj-actions/changed-filesの事件が示したとおり、タグは後から別のコミットを指すように書き換えられます。GitHubの文書は、サードパーティのアクションを完全な長さのコミットSHAで固定し、そのSHAがフォークではなくアクション本来のリポジトリのものであることを確かめるよう求めています。

# タグ参照は、タグが書き換えられると中身が変わる
- uses: example-org/example-action@v4

# コミットSHAで固定し、版はコメントで残す
- uses: example-org/example-action@<40桁のコミットSHA>  # v4.2.1

SHAで固定すると更新が止まりがちなため、DependabotやRenovateで更新のプルリクエストを受け、差分を確かめてから取り込みます。GitLab CIでも同じ考え方で、GitLabの文書は次の固定を勧めています。

  • コンテナイメージは、タグではなくSHAのダイジェストで指定する。イメージの指定に変数を使わない
  • includeで読み込む設定は、特定のコミット、保護タグ、コンポーネントの版で固定する。版を指定しない読み込みを避ける
  • 依存パッケージはロックファイルで固定し、npm ciやyarn install --frozen-lockfileを使う
  • ジョブで取得するツールは版を指定し、チェックサムで検証する

依存パッケージを狙う攻撃の全体像は、次の記事で解説しています。

あわせて読みたい

サプライチェーン攻撃の構造と防御の考え方。ソフト・ハード・サービス経由の侵入をどう減らすか

Runnerを使い捨てにして分離する

ジョブを実行するRunnerは、悪意あるジョブが動いたときに、その被害をどこまで広げるかを決める場所です。GitHubの文書は、自前のRunner(self-hosted runner)を公開リポジトリにはほぼ使うべきでないとし、自前のRunnerは毎回きれいな仮想マシンで動く保証がないため、持続的に侵害されうると説明しています。GitLabのRunnerの文書も、次の点を挙げています。

設定GitLabの文書の説明
shell executorRunnerのホストとネットワークへの危険が高い。同じサーバー上のほかのプロジェクトのコードにも届くため、信頼できるビルドだけに使う
特権(privileged)コンテナ推奨しない。必要な場合は、隔離された使い捨ての仮想マシンだけで動かす
特権が必要なジョブ保護ブランチのジョブだけを実行するRunnerに分け、汎用のRunnerと分離する
使い捨ての構成自動で増減する仮想マシンで、1台あたりのジョブを1件に限る
ネットワークジョブを専用のネットワークセグメントで動かし、悪意あるジョブが届く範囲を狭める
RunnerのトークンRunnerの識別に使う機密情報として扱い、Runnerを複製しない

複数のプロジェクトで使い回す常駐型のRunnerは、1つのジョブの侵害がほかのプロジェクトのジョブへ波及する危険が大きくなります。本番へのデプロイを担うRunnerは、ほかの用途と分け、到達できる宛先を本番環境のデプロイ先に限ります。

CI/CDの基盤そのものを更新し、記録を残す

パイプラインの設計をいくら固めても、CI/CDの基盤(GitLab、Jenkins、TeamCityなど)自体に脆弱性があれば、基盤のサーバーから秘密情報と権限をまとめて奪われます。2026年に入ってからも、GitLabのCVE-2026-85706とCVE-2026-97229、TeamCityのCVE-2026-63077など、CI/CDの基盤の脆弱性が続けて悪用の対象になっています。セルフマネージドの基盤は本番環境と同じ優先度で更新し、インターネットからの接続元を絞ります。

あわせて読みたい

GitLabの脆弱性CVE-2026-97229とは、CI/CDの正規表現でRubyのOnigmoを壊すRCEと修正版

記録の面では、次の操作を監査ログで追えるようにし、想定外の変更を通知します。

  • パイプラインの定義ファイルと、保護ブランチの設定の変更
  • CI/CDの変数、秘密情報、OIDCの信頼設定の追加と変更
  • Runnerの登録と削除、アクセストークンの発行
  • 本番環境へのデプロイと、その承認者

導入の順番

すべてを一度に入れる必要はありません。効果の大きいものから順に進めます。

  1. 1

    棚卸しをする

    パイプラインごとに、渡している秘密情報とトークンの権限、使っているサードパーティのアクションやイメージ、実行しているRunnerを一覧にします。長期のアクセスキーがどこにあるかも書き出します。

  2. 2

    定義ファイルと外部からのコードを守る

    保護ブランチとコードオーナーで定義ファイルの変更を絞り、pull_request_targetや親プロジェクトでのフォークのパイプラインの実行を見直します。

  3. 3

    権限を絞り、秘密情報を短命にする

    ジョブのトークンを読み取りに絞り、クラウドの認証をOIDCへ移します。移せない秘密情報は保護とマスクを付け、ローテーションの周期を決めます。

  4. 4

    参照を固定し、Runnerを分ける

    アクションをコミットSHAで、イメージをダイジェストで固定し、自動更新の仕組みを入れます。本番のデプロイ用のRunnerを使い捨ての構成で分離します。

  5. 5

    基盤の更新と監視を続ける

    CI/CDの基盤のセキュリティ情報を購読して更新を続け、監査ログで定義ファイルと秘密情報の変更を追います。

対応チェックリスト

  • パイプラインの定義ファイルを、保護ブランチとコードオーナーのレビューの対象にしているか
  • フォークからのプルリクエストのコードを、秘密情報や書き込み権限のある文脈で動かしていないか
  • 外部の人が決められる値を、${{ }}でrunのスクリプトへ直接埋め込んでいないか
  • GITHUB_TOKENやCI_JOB_TOKENの権限を、必要な範囲に絞っているか
  • クラウドへの認証をOIDCに移し、信頼設定の主体をリポジトリとブランチまで絞っているか
  • サードパーティのアクションをコミットSHAで、コンテナイメージをダイジェストで固定しているか
  • 自前のRunnerを公開リポジトリに使わず、本番のデプロイ用を使い捨ての構成で分離しているか
  • CI/CDの基盤を最新に保ち、定義ファイルと秘密情報の変更を監査ログで追えるか

パイプラインは本番環境の一部として設計する

CI/CDのパイプラインは、開発の道具であると同時に、本番環境とソフトウェアの利用者へ届く経路です。誰がパイプラインの中身を変えられるか、ジョブに何を渡しているか、ジョブがどこで動くかの3つを把握し、それぞれを最小にすることが守りの出発点になります。SASTやシークレットスキャンなど、パイプラインに検査を組み込む方法は次の記事で解説しています。

あわせて読みたい

DevSecOps入門。CI/CDにセキュリティを組み込むSAST・DAST・SCA・シークレットスキャン

出典・参考

この記事をシェア

関連する記事

脆弱性・CVE解説

GitLabの脆弱性CVE-2026-97229とは、CI/CDの正規表現でRubyのOnigmoを壊すRCEと修正版

GitLab 19.2から19.4のCI/CD設定の正規表現を通じて、Rubyの正規表現エンジンOnigmoの整数オーバーフローを突きコードを実行できるCVE-2026-97229がKEVに追加されました。CVSSは10.0です。仕組み、関連する3件のCVE、修正版、Rubyアプリへの影響、点検の手順を一次情報で整理します。