CI/CDを分離した権限設計について考えてみる

こんにちは。インフラ担当の小林匠です。

CI/CDのワークフローを書いているときに、「このワークフローってCIなのかCDなのか、どっちなんだろう」とふと考えることはないでしょうか。
かくいう私ですが、以前まであまり深く考えずにCI/CDを一つのワークフローに詰め込んでいました。
しかし、あるきっかけで「分離した方がいいのでは」と考えを改める場面がありました。

それは、先日開催された JAWS DAYS 2026 のセッション「クラウドネイティブ時代のウェブセキュリティ再考」を聴講した時です。

www.youtube.com

ここで、CI/CD環境におけるサプライチェーン攻撃のリスクをはじめとした、Codecov事件やTrivyコンテナ削除事件など、実際に起きたインシデントから学ぶことが多いと感じました。

本記事では、CI/CDパイプラインの設計を考えていく上で、CIとCDを明確に分離して設計することで、安全性・保守性・拡張性を高めることが期待できる実践的なアプローチをセキュリティの観点から整理してみようと思います。

また、AWSインフラ(Terraform利用)のCI/CDパイプラインの設計・運用を考える際に行き着いた知見や考えをもとに、GitHub Actionsを用いた具体的な実装パターンと設計上の重要なポイントが見えてきたので、まとめていきます。

そもそもインフラにおけるCI/CDとは何か

インフラコードにおいて、CI(継続的インテグレーション)CD(継続的デリバリー/デプロイ)は明確に異なる責務を持ちます。

CIの役割

  • コードの検証(terraform validate, trivy等によるスキャン)
  • 変更内容の可視化(terraform planによる事前確認)
  • 問題の早期発見
  • 実際にインフラを変更しない(操作:Read Only)

※ただし、Terraformの構成によってはPlan実行時にStateのロック取得(DynamoDBなど)や、外部データの参照に伴う副作用が発生する場合があります。実プロジェクトではPlanに必要な権限をCloudTrail等で確認し、厳格な最小権限を設計することが重要です。

CDの役割

  • 検証済みコードの適用(terraform applyによるリソース作成)
  • 承認プロセスの実装(Environment Protection Rules等による制御)
  • 実際にインフラを変更する(操作:Write)

ここで最も重要なことは、インフラのCDは実行するだけで本番環境が変更されるという点です。
アプリケーションのデプロイ(コードの差し替え)と比較して、データベースの削除やネットワーク設定の誤りなど物理的な破壊や通信遮断を伴うため復旧への影響度が大きいため、CIで確実に検証を行い、CDでは検証をもとに慎重に適用する設計が求められると考えます。

CI/CDの責務分離

JAWS DAYS 2026 から学ぶ、CI/CDセキュリティの死角

セッションでも取り上げられていましたが、CI/CD環境は攻撃者にとって魅力的なターゲットといえます。
なぜなら、CI/CD環境は本番環境への直接アクセス権を持つからです。

サプライチェーン攻撃の実例

1.Codecov事件(2021年)
Codecovのbashアップローダーが改ざんされ、CI環境から顧客の機密情報(環境変数、シークレット)が窃取されました。 メルカリも被害を受けた事例として知られています。

影響範囲

  • CIで使用していた環境変数(APIキー、トークン)が漏洩した
  • テストデータとして埋め込まれていたシークレットも流出した
  • CI環境が汚染されたため、本番環境への影響も懸念された

学び

  • テストデータやソースコード内に、実際の個人情報や本番環境の機密情報(シークレット)を絶対に埋め込まない(マスクやモック、ダミーデータを使用する)
  • 外部ツールのスクリプト等はバージョン(またはコミットハッシュ)をピン留めし、実行時に動的に未検証のコードをインクルードしない

2.Trivy削除事件(2024年)

Trivyの開発元である組織のGitHubリポジトリにおいて、悪意のあるPull Requestによってリポジトリ自体のデータやリリース、タグがハッカーボットによって大量に削除される事象が発生しました。

原因

外部からのPR(Pull Request)であっても、特定のトリガー(pull_request_target)の使い方や権限設定の不備により、GitHubのシークレットやリポジトリへのWrite(書き込み)権限を持つトークンが攻撃者のスクリプトへ漏洩してしまったこと

学び

  • 外部からのPRを受け付けるワークフローでは、トリガーの選定(pull_request と pull_request_target の違い)に細心の注意を払う
  • 「コンテナスキャンを実行するだけ」のワークフローに、リポジトリの削除・変更ができる Write 権限や強力なシークレットは不要であり、デフォルト権限は最小限(Read Only)に絞るべきである

紹介されていた対策の3原則

  1. PRは弱い権限で実行させること
    Pull Request トリガーのワークフローには、読み取り専用の権限のみを付与すること。

  2. バージョンのピン留めを使用する
    GitHub Actions や依存ツールは@latest ではなく、明示的なバージョンを指定すること。
    実務では @v4 のようなメジャーバージョン指定が一般的ですが、より厳格なサプライチェーン対策が必要な場合は、改ざんリスクを排除するために commit SHA による指定を検討します。その際はDependabot等を併用し、更新管理を自動化するのが現実的です。

  3. OIDCを使用する(アクセスキー禁止)
    長期的なクレデンシャルではなく、短命なトークンで認証すること

DevOpsにおける脆弱性診断の種類

セキュアなCI/CDには、複数の診断手法を組み合わせる必要があります。

診断手法 対象 実施タイミング 検出内容
SAST ソースコード CI時 コード内の脆弱性パターン
SCA 依存ライブラリ CI時 既知の脆弱性を持つライブラリ
コンテナスキャン コンテナイメージ ビルド時 イメージ内の脆弱性
DAST 実行中のアプリ CD後 実行時の脆弱性

CIとCDを分離する設計思想

最初、私はCI/CDを一つのワークフローにまとめていました。しかし、これには根本的な問題がありました。

  • 最小権限の原則との乖離(過剰な権限):CI実行時に本番環境への書き込み権限が必要になる

  • 承認後の実行のみを切り出せないため、レビュー(検証)と適用を段階的に制御することが難しい:検証と適用が一体化している

  • サプライチェーン攻撃への脆弱性:PRを作るだけで本番環境へアクセスできる

これらは、「検証」と「適用」という異なる責務を一つで扱おうとしたことに起因していました。

分離による責務定義

ワークフロー トリガー 権限 主な目的
CI Pull Request Read Only 変更内容の検証・可視化
CD mainへのマージ Write 検証済みコードの適用

上記のように分離することで、「CIは何度実行しても安全である」「CDは慎重に承認をもって実行される」という状態が実現可能となります。
特に重要なのが、CIとCDの「権限」を分離しておくことです。
もしCodecov事件のようなサプライチェーン攻撃によってCI環境が侵害されたとしても、CI側に本番環境への「Write(書き込み)権限」を付与していなければ、本番インフラの破壊や改ざんといった最悪の直接被害を防ぐ(リスクを限定する)ことができます。
ただし、メルカリの事例が示すように「Read権限」だけでも機密情報の漏洩リスクは残るため、環境変数に無駄なシークレットを載せない、テストデータに本物のデータを含めないといった対策との多層防御が不可欠です。
また、CI/CDを分けることは単にファイルを分けることではありません。本番環境への変更を「誰が・いつ・何を」に基づいて行うかを明確に制御するゲートを作るということです。

CIパイプラインの設計方針

トリガー設計

on: 
  pull_request:
    branches: [main]
    paths: ['terraform/**']
  workflow_dispatch:

Jobの構成(Fail Fastとセキュリティ)

jobs:
   validate: # 構文チェック
    runs-on: ubuntu-latest
    permissions: 
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init -backend=false 
      - run: terraform fmt -check -diff -recursive 
      - run: terraform validate

security: # セキュリティスキャン
    needs: validate 
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master 
        with:
          soft_fail: false

plan: # Terraform plan
    needs: security
    permissions: 
      id-token: write
      contents: read 
      pull-requests: write 
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS (OIDC)
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME }}
      - run: terraform plan 
      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          retention-days: 5

※ ここで設定している id-token: write は、GitHub側でOIDCトークンを発行するための権限です。これによりAWSへの一時的なアクセスが可能になりますが、宛先のIAMロール側を「Read Only」に絞っていれば、AWSリソースが書き換えられる心配はありません。
※ Planファイルにはインフラの構成詳細が含まれるため、Artifactの保持期間や閲覧権限には注意が必要です。

脆弱性が見つかった場合、後続Jobは実行しない

重要な設計ポイント

1. セキュリティスキャンを必須化

  • soft_fail: falseで、脆弱性検出時は必ずCIを失敗させる

  • これにより、セキュリティ問題のあるコードがマージされることを防ぐ

2. バージョンピン留め

前段の「3原則」でも触れましたが、タグ(@v4など)は後から書き換えられるリスク(タグジャンプ)がゼロではありません。
サプライチェーン攻撃を厳格に防ぐには、以下のように変更不可能な「コミットハッシュ(SHA)」で指定するのが最も安全です。

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2のSHAを例に

※運用の手間とのトレードオフになるため、Dependabot等を併用して自動更新する仕組みとセットで導入します。

3. CI専用IAMロール(Read Only)
OIDCの信頼ポリシーでは、sub(どのリポジトリか)だけでなく、aud(接続先がAWSである証明)も必ずセットで検証するように設定します。

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts:amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:org/repo:pull_request"
    }
  }
}

CDパイプラインの設計方針

トリガー設計

on: 
  push:
    branches: [main]
    paths: ['terraform/**']
  workflow_dispatch: # メンテナンス用に手動実行

concurrency:
  group: terraform-production
  cancel-in-progress: false

段階的デプロイと承認フロー

jobs:
  deploy-dev: 
    environment: dev
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS (OIDC)
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT:role/cd-dev
          aws-region: ap-northeast-1
      - run: terraform apply -auto-approve

  deploy-staging: 
    needs: deploy-dev
    environment: staging
    # 構成は省略

  deploy-production: 
    needs: deploy-staging
    environment: production
    # 構成は省略

Environment Protection Rules

GitHub上で環境ごとに設定可能、参考として

環境 承認者数 待機時間 備考
開発 0 なし 自動デプロイ
検証 1 なし チームリーダー
本番 2 5分 チームリーダー、PMなど

待機時間は、承認ボタンを押した瞬間に実行されるのではなく、5分間の猶予を設けることにより誤承認や、承認後に懸念点に気づいた際、実行前に踏みとどまれる猶予を持たせるためです。

OIDC信頼ポリシーの環境別設計

CD用のIAMロールは環境ごとに分離

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:org/repo:environment:production"
    }
  }
}

これにより、GitHub上で本番環境として承認されたジョブからのみ本番ロールを引き受けることが可能となります。

まとめ

CIとCDを分離する設計は、一見複雑に見えるかもしれません。しかし、実際に運用してみると、この分離が安全性と俊敏性を両立させることを可能にします。
特に、JAWS DAYS 2026 で紹介されていたサプライチェーン攻撃の事例を知ると、CI/CDのセキュリティ設計の重要性が一層明確になると痛感します。

CI/CDに限る話ではないですが、私は技術的な実装以上に重要なのは「なぜそう設計するのか」を言語化することであると考えます。
また、実務を通じて感じたのは、CI/CDパイプラインは一度構築すると後からセキュリティを強化することがなかなか難しいと感じます。
わかりやすいところでいえば、変更による既存ワークフローが動かなくなる懸念があることです。

だからこそ、最初から安全な設計が重要であると結論づけることができました。

また、今回JAWS DAYSに初めて参加したのですが、本記事で記述したこと以外にも有益な情報がたくさんありました。
いうまでもないですが、最新技術のキャッチアップや過去事例などから学ぶことは重要なことであると感じました。

それではまた。

参考資料

Bash Uploader Security Update - Codecov
OWASP Top 10 CI/CD Security Risks | OWASP Foundation
Quickstart for securing your repository - GitHub Docs
Learn Terraform recommended practices | Terraform | HashiCorp Developer