🐙

terraform applyをGHAで実行してはいけない理由

に公開

terraform applyをGHAで実行してはいけない理由

こんにちは、SREの@okazu_dmです。
常日頃からGHAでterraform applyをするのはやめろと言い続けているのですが、最近GHAを起点とした攻撃が立て続けに起こっており、改めて文章の形でまとめておいた方がいいなと思ったので記事を書きました。

はじめに

2026年3月ごろから、GHAを攻撃の入り口として悪用する事例が目立っています。

たとえば、以下のAeye Security Labの記事では、PRタイトル、ブランチ名、ファイル名など、外部から与えられる値をGHAの run ステップ内で不適切に展開した場合に、任意コマンド実行やシークレット漏洩につながることが検証されています。

ここで重要なのは、攻撃対象がpublic repositoryに限られないことです。private repositoryであっても、開発者、外部委託先、連携アプリケーション、漏洩したGitHubトークン、侵害された依存Actionなど、GHAに到達する経路はいくつもあります。

GHAは手軽で便利なCI/CD基盤ですが、任意のコマンドを実行し、シークレットを読み取り、外部サービスの認証情報を取得できる実行環境でもあります。そのGHAにAWSアカウント全体を変更できる権限を渡し、さらにterraform applyまで自動実行させる構成は、特にGHAの攻撃経路としての注目度が高まっている現在では極めて危険であると考えます。

この記事における前提

以降では、2026/04時点で以下のようなツール/サービスの組み合わせの話をします

  • Terraform v1.14.x
  • AWS

また、Terraformの実行対象は本番環境またはそれに準じる重要なAWSアカウントを想定します。個人検証用の使い捨てアカウントではなく、VPC, IAM, RDS, EC2, S3, CloudFront, Route 53などをTerraformで継続管理しているような環境です。

この記事では、terraform planをPRレビューで扱っているケースなどはどちらでも良いとします(直ちにwriteの操作や機密情報の漏洩につながるものではないため)。
以降で問題として扱うのは、最終的にAWSへ変更を反映するterraform applyを、GHAのジョブから直接実行してよいのか、という点です。

脆弱なGHAの例

まず、ありがちな構成を見ます。

name: terraform-apply

on:
  push:
    branches:
      - main
    paths:
      - infra/**
      - .github/workflows/terraform-apply.yml
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  apply:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6.1.0 # SHAでpinningしないと危ないよ、などの観点はここでは無視する
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-terraform-apply # アクセスキーではなくOIDCでAssumeRoleする構成を想定
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.14.0

      - name: Terraform init
        run: terraform init -input=false

      - name: Terraform apply
        run: terraform apply -auto-approve -input=false

OIDCを使っており、長期のAWSアクセスキーをGitHub Secretsに置いていません。permissionsid-token: writecontents: readに絞っています。push対象もmainだけです。ぱっと見では、かなりまともに見えます。

しかし、このworkflowはmainに入ったTerraformコードをGHA上で自動的にapplyします。つまり、mainに入る経路、GHAの実行環境、workflow内で実行されるAction、Terraform実行前後の任意のステップのどれかが崩れると、そのままAWSへの変更権限に到達します。

さらにworkflow_dispatchを付けている場合、手動実行できる人の権限設計も問題になります。GitHub上でのworkflow実行と、AWS上のインフラ変更作業は、本来は同じ権限で扱うべきではありません。

GHAにおけるAWSへのアクセス権限の扱い

上記のワークフローの問題について考える前に、GHAでAWSを操作する際の認証方法について簡単に説明します。
aws-actions/configure-aws-credentialsでOIDCを使う場合、おおまかな流れは次のとおりです。

  1. workflowにpermissions: id-token: writeを付与する
  2. GHAジョブ内でGitHubのOIDCトークンを取得する
  3. AWS IAM側で、GitHubのOIDC Providerと信頼するIAM Roleを用意する
  4. aws-actions/configure-aws-credentialsがAssumeRoleで一時認証情報を取得する
  5. 取得したAWS認証情報が、後続のterraform initterraform planterraform applyから使われる

公式READMEでも、OIDCは推奨構成として説明されています。長期アクセスキーをGitHub Secretsに保存するより安全であることは確かです。短期認証情報であり、IAM Roleの信頼ポリシーでsubaudを絞れるためです。

ただし、これは「AWS認証情報をどう渡すか」の改善であって、「GHAに強いAWS権限を渡してよいか」という話とは別問題です。
つまり、権限を渡す経路と、渡される権限そのものの妥当性は別々に考える必要があります。

デフォルトでは、このActionはAWS認証情報を環境変数としてセットします。後続ステップからは、AWS CLI、Terraform、任意のスクリプトが同じ認証情報を利用できます。つまり、terraform applyを実行できるジョブ内で任意コマンド実行が成立した場合、そのコマンドも同じAWS権限を持ちます。

OIDCは、漏洩した長期アクセスキーが残り続ける問題を緩和します。しかし、侵害されたジョブ実行中にAWS APIを呼ばれる問題は緩和しません。攻撃者にとっては、ジョブの実行時間内にIAM Roleを使ってAWS APIを叩ければ十分です。

そもそもIaCによるインフラ管理は莫大な権限を要求する

TerraformでAWSを広く管理する場合、実運用上はAdmin相当の権限が必要になります。

もちろん、技術的にはTerraformが作成、更新、削除するすべてのリソースとAPIを洗い出し、IAM Policyを最小権限で作り込むことは可能です。しかし、実際の継続運用では、管理対象リソースは増えます。Providerの実装都合で読み取りAPIも必要になります。既存リソース参照、タグ更新、IAM Role作成、KMS Key操作なども出てきます。

これらの最大公約数を取るための粒度の細かいIAM設計をするコストは、組織規模やサービスの運用ポリシーにもよりますが作業コストの面で合理的とは言い難いです。
結果として、一般的な運用では本番AWSアカウントの主要なインフラをTerraformで管理する実行ロールは、かなり広い権限を持たざるを得ません。少なくとも、通常のアプリケーション実行ロールやCIのテスト用ロールとは桁が違います。

CloudFormationを使えば、呼び出し元にはスタック操作の権限だけを与え、実際のリソース操作はCloudFormationのサービスロールに寄せる構成も取れます。見かけ上、呼び出し元の権限は狭くできます。しかし、そのサービスロールに付与された権限は、やはり管理対象インフラを作成、更新、削除できるだけの強い権限です。

これはTerraformやCloudFormationの欠陥ではありません。管理者としてインフラを変更するには、管理者相当の権限が必要という当然の構造です。議論すべき問題は、その強い権限をどの実行環境から呼び出せるようにするかにあります。

GHAでterraform applyを自動実行する構成では、その強い権限を(しばしば意図しないうちに)GHAのジョブに渡しています。したがって、GHAの侵害は単なるCIの侵害ではなく、AWS管理プレーンの侵害になります。

想定される被害

典型的な被害の流れは次のようになります(そもそも1の時点でだいぶ事態は深刻ですが....)。

  1. 攻撃者がGHA上で任意のコマンドを実行できる状態を作る(例: 依存Actionの侵害、pull_request_targetの誤用、開発者アカウントの侵害など)
  2. そのジョブはterraform apply用のAWS IAM Roleを引き受けられる
  3. 攻撃者はジョブ内でAWS APIを直接呼ぶ、またはTerraformコードを書き換えてapplyさせる
  4. IAM Role、IAM User、Access Key、Security Group、Lambda、S3 Bucket Policyなどを変更する
  5. 永続的なバックドア、データ窃取、ログ削除、追加の権限昇格につながる

たとえば、依存しているActionが侵害され、terraform applyの前にTerraformファイルを書き換えるケースを考えます。workflow上はいつもどおりterraform applyが成功したように見えますが、実際にはIAM Policyに余分な権限が追加されていたり、攻撃者が使う外部アカウントにAssumeRoleを許す信頼ポリシーが追加されていたりする可能性があります。

ここで問題にしているのは、pushをトリガーにした自動applyだけではありません。ChatOps的に「PRのコメントに/applyと書いたらGHAがterraform applyする」などの構成も、同じ種類のリスクを持ちます。

どちらも、GitHub上のイベントや権限を起点にして、最終的なAWS変更権限を動かしているからです。

また、開発者のGitHubアカウントや個人アクセストークンが漏洩した場合も危険です。その開発者がworkflow実行、ブランチ操作、レビュー承認、環境承認のいずれかを行えるなら、GitHub上の権限がAWS変更権限に変換される経路になります。

ここでの本質は、GHAの実行権限とAWSの変更権限が結合していることです。GHAを奪えばAWSを奪える、という構造を作ってはいけません。

対策の方針: 自動で実行を諦める

対策の方針は単純です。GHAからterraform applyまでを全自動でつなぐことを諦めます。

GHAの発火から実際のterraform applyまでの間に、GHAから操作できない範囲で手動承認を挟みます。ここでいう「GHAから操作できない範囲」とは、GitHub repositoryの設定、GitHub Environments、workflowファイル、GitHub APIだけでは変更できない境界のことです。

GitHub EnvironmentsのRequired reviewersは、何もない全自動構成よりは明らかにマシです。GitHub公式ドキュメント上も、環境にRequired reviewersを設定すると、その環境を参照するジョブは承認されるまで進まないと説明されています。また、Prevent self-reviewや、管理者による保護ルールバイパスの禁止も設定できます。

しかし、これはあくまでGitHub内の保護です。環境設定、workflow定義、repository管理者権限、GitHub連携アプリケーションが同じ管理プレーンにあります。GitHub側の侵害や管理者権限の悪用を前提にすると、最終防衛線としては弱いです。

したがって、terraform applyの直前に置く承認は、GitHubの外側に置くべきです。AWSで完結させるなら、CodePipelineのManual approval actionを使い、その後段のCodeBuildでterraform applyを実行する構成が考えられます。

承認ステップをどこに置くかという話と共に重要なのは、承認者の権限です。この承認は、terraform applyに必要な権限と同等の重みを持ちます。つまり、承認権限自体は限定的な権限であっても承認者はその変更対象の管理権限を持っていることと同様になります(この点は、GHAの場合と同様です)。実際の管理権限の持ち主以外に承認をさせたい特別な理由がない限りは、承認者は管理者に限ることを推奨します。

対策の実装例: CodePipelineによる手動承認 + CodeBuild

ここでは、AWS内に承認境界を置く構成例を示します。

全体の流れは次のとおりです。

  1. GHAはterraform applyを実行しない
  2. GHAはCodePipelineを起動するだけにする
  3. CodePipeline内でManual approval actionを挟む
  4. 承認後、CodeBuildがTerraformコードを取得してterraform applyを実行する

この構成では、GHAに渡すAWS権限をcodepipeline:StartPipelineExecutionなどに限定できます。Terraformを実行する強い権限は、CodeBuildのサービスロールに持たせます。

GHA側は、たとえば次のようにします。

name: request-terraform-apply

on:
  push:
    branches:
      - main
    paths:
      - infra/**
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  request-apply:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v6.1.0
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-start-terraform-pipeline
          aws-region: ap-northeast-1

      - name: Start CodePipeline
        run: |
          aws codepipeline start-pipeline-execution \
            --name terraform-apply-production

このRoleには、Terraform実行権限を付けません。例としては次のような権限にします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "codepipeline:StartPipelineExecution",
      "Resource": "arn:aws:codepipeline:ap-northeast-1:123456789012:terraform-apply-production"
    }
  ]
}

CodePipeline側には、Source、Manual approval、Buildの3段を置きます。

GitHub Actions
  ↓ codepipeline:StartPipelineExecution
CodePipeline Source

Manual approval
  ↓ 承認者がAWS側で承認
CodeBuild terraform apply

CodeBuildでは、Terraform v1.14.0を含むカスタムイメージを使う前提で、たとえば次のようなbuildspec.ymlでTerraformを実行します。

version: 0.2

env:
  variables:
    TF_IN_AUTOMATION: "true"

phases:
  pre_build:
    commands:
      - terraform version
  build:
    commands:
      - cd infra
      - terraform init -input=false
      - terraform apply -auto-approve -input=false

この例は最小構成です。実際には、Terraformを含むビルドイメージの管理、Terraformのバイナリ検証、Plugin Cache、State Lock、CodeBuildのVPC設定、ログ保管、通知、承認時の差分確認、監査ログの保存などを追加で設計する必要があります。

それでも、GHAが直接AWS管理者相当のRoleを引き受ける構成よりは、権限境界が明確になります。GHAができるのは「applyの依頼」までであり、「applyの実行」はAWS側の承認を通過しないと起きません。

別案: HCP Terraform Cloudのような専用実行基盤を使う方法

別案として、HCP Terraformの手動承認を使う方法もあります。HCP TerraformのUI/VCS-driven run workflowでは、デフォルトではplan後にユーザーがConfirm & ApplyまたはDiscard Planを選び、apply権限を持つユーザーが適用を確定する流れが説明されています。Terraform実行基盤をHCP Terraformに寄せられる組織では、こちらも有力な選択肢です。

考察

正直に言うと、この構成は面倒です。

GHAだけで完結していたときは、workflowファイルを読めばだいたい全体像がわかりました。CodePipelineとCodeBuildを挟むと、GitHub、IAM、CodePipeline、CodeBuild、CloudWatch Logs、通知、承認権限をまたいで理解する必要があります。

しかし、その「シンプルで便利な自動化」には致命的な隙があります。GHAでテストもビルドもデプロイもインフラ変更も全部やる、という構成は運用上はシンプルで気持ちいいのですが、侵害時の被害半径も大きくなります。

CodePipelineは素朴で、UIも現代的とは言いにくいです。CodeBuildも起動が速いとは言えません。Cloud Runのように、短時間で起動してすぐ処理できる実行基盤と比べると、AWSにももう少し頑張ってほしい気持ちはあります。

ただし、ここで必要なのは、洗練されたCI体験ではなく、強い権限を行使する直前の境界です。承認ステップをGitHubの外に出すという一点では、CodePipelineとCodeBuildは十分に役割を果たせます。

もちろん、承認ステップを分けるだけなら、別の実行基盤を組み合わせることもできます。しかし、あまりに凝った自作の承認基盤を作ると、その基盤自体が新しい攻撃面になります。ここでは、多少つらくても、AWSの管理プレーン上に寄せる方がまだ説明しやすいと考えています(ここでは例としてAWSの話をしたが、例えばGoogle Cloudの場合はGoogle Cloudに閉じて作るなど)。

まとめ

terraform applyをGHAで直接実行する構成は危険です。

OIDCを使っていても、長期アクセスキーを置いていなくても、mainマージ後だけに絞っていても、問題の本質は残ります。GHAのジョブがAWS管理者相当の権限を取得し、そのままインフラ変更を実行できるからです。

特に近年は、GHAを起点にした攻撃が目立っています。信頼できない入力の展開、pull_request_targetの誤用、依存Actionの侵害、開発者アカウントの侵害など、GHAの実行環境に到達する経路はいくつもあります。

必要なのは、ジョブの発火と実際のインフラ変更の間に、GHAから操作できない手動承認ステップを置くことです。そして、その承認はterraform applyに必要な権限と同等の重みを持つ人が行う必要があります。

自分の現時点の結論は、GHAではCodePipelineを起動するだけにし、CodePipeline内のManual approvalを通過した後にCodeBuildでterraform applyする、というものです。HCP Terraform(旧Terraform Cloud)のような専用実行基盤を使えるなら、それも有力です。

また、今回は手動承認が必須、という書き方をしましたが、あたたかみのある手動承認以外の方法があればぜひコメントなどで教えてください。

参考文献

Discussion