Security Command Center の IaC 検証を GitHub Actions で実行してみた

Security Command Center の IaC 検証を GitHub Actions で実行してみた

2026.06.09

前回、Security Command Center(SCC)の IaC 検証を gcloud scc iac-validation-reports create で手元から叩き、違反入りの Terraform plan がどんな JSON を返すのかを試しました。

https://dev.classmethod.jp/articles/gcp-scc-iac-validation-terraform/

今回はこれを GitHub Actions に組み込みます。PR を出したタイミングで Terraform plan を自動検証し、違反があれば job を落として PR を止めます。

検証結果の SARIF は artifact として保存し、ダウンロードして中身を確認します。

参考にしたのは GitHub Actions 連携の公式チュートリアルです。

https://docs.cloud.google.com/security-command-center/docs/iac-validation-github-actions

使うもの

CI で Google 製の GitHub Action google-github-actions/analyze-code-security-scc を使います。

https://github.com/google-github-actions/analyze-code-security-scc

ローカルで gcloud scc iac-validation-reports create を直接叩く方法もありますが、CI ではこの専用 Action を使うのが標準です。

前提

  • SCC が Premium または Enterprise で、組織レベルで有効化されていること(プロジェクトレベルでは使えません)
  • 検証用のセキュリティポスチャー(組織ポリシーや Security Health Analytics のモジュールをまとめたもの)を組織で作成し、対象プロジェクトにデプロイしておくこと。IaC 検証はデプロイ済みポスチャーに対して走るので、これが無いとカスタムルール分の違反が出ません
  • API は securityposture.googleapis.comsecuritycentermanagement.googleapis.com を有効化済み
  • GitHub リポジトリに、ポスチャーのルールに違反する main.tf を置く

以降の <...> は環境に合わせて置き換える値です(<ORGANIZATION_ID><PROJECT_ID><PROJECT_NUMBER><POOL_ID> など)。

サービスアカウントとロールを準備する

SA と WIF を Terraform で作ります。

ただし PR でスキャンする terraform/ とは別構成にし、ステートファイルも分けます。

スキャン対象と混ぜると、ブートストラップ用のリソースまで検証されてしまいます。CI が動く前に存在している必要がある設定でもあるため、置き場所を分けました。

ここでは bootstrap/ に置きます。

bootstrap/   # SA・WIF(このセクション。スキャン対象とは別ステート)
terraform/   # スキャン対象(違反する main.tf)

provider と変数を定義します。

bootstrap/terraform.tf
terraform {
  required_version = ">= 1.15"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.0"
    }
  }
}
bootstrap/variables.tf
variable "project_id" {
  description = "サービスアカウントと WIF プールを作るプロジェクト ID"
  type        = string
}

variable "organization_id" {
  description = "Shift-Left Validator ロールを付与する組織 ID"
  type        = string
}

variable "github_repository" {
  description = "認証を許可する GitHub リポジトリ(owner/repo 形式)"
  type        = string
}

variable "pool_id" {
  description = "Workload Identity プール ID"
  type        = string
  default     = "github-pool"
}

variable "provider_id" {
  description = "Workload Identity プロバイダ ID"
  type        = string
  default     = "github-provider"
}

検証用サービスアカウントを作り、組織レベルで「Security Posture Shift-Left Validator」ロール(実 ID は roles/securityposture.reportCreator)を付与します。

実機でハマったのは付与先です。このロールは組織レベルでしか付与できません。プロジェクトに付けると Role ... is not supported for this resource で弾かれます。IaC 検証レポートは organizations/<ORG>/locations/global 配下のリソースで、gcloud scc iac-validation-reports create も Action も org スコープしか取りません。権限は組織に対して評価されるので、付与先も組織になります。ロールの権限は securityposture.reports.create / get / listoperations.get だけで、検証レポートの作成と参照に限られます(書き込みや enforce はできません)。

bootstrap/main.tf
# 検証用サービスアカウント
resource "google_service_account" "scc_iac_validator" {
  project      = var.project_id
  account_id   = "scc-iac-validator"
  display_name = "SCC IaC Validator"
}

# 組織レベルで Shift-Left Validator ロール(実 ID は reportCreator)を付与
resource "google_organization_iam_member" "report_creator" {
  org_id = var.organization_id
  role   = "roles/securityposture.reportCreator"
  member = "serviceAccount:${google_service_account.scc_iac_validator.email}"
}

組織レベルの IAM 設定には resourcemanager.organizations.setIamPolicy(Organization Administrator 相当)が要ります。terraform apply を実行するアカウントにこの権限が無いと、この binding だけ失敗します。

Workload Identity 連携を設定する

GitHub Actions から鍵レスに認証するため、同じ bootstrap/ にプールとプロバイダを足し、GitHub の OIDC を信頼させます。サービスアカウントキー JSON は使いません。bootstrap/main.tf に以下を追記します。

bootstrap/main.tf
# Workload Identity プール
resource "google_iam_workload_identity_pool" "github" {
  project                   = var.project_id
  workload_identity_pool_id = var.pool_id
  display_name              = "GitHub Actions Pool"
}

# GitHub OIDC プロバイダ
resource "google_iam_workload_identity_pool_provider" "github" {
  project                            = var.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = var.provider_id
  display_name                       = "GitHub Actions Provider"

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
  }

  # 指定したリポジトリ以外のトークンを弾く(無いと全リポジトリを信頼してしまう)
  attribute_condition = "assertion.repository == '${var.github_repository}'"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

# 該当リポジトリの principal に workloadIdentityUser を付与
resource "google_service_account_iam_member" "wif_binding" {
  service_account_id = google_service_account.scc_iac_validator.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.github_repository}"
}

ワークフローに貼る値は output で受け取ります。

bootstrap/outputs.tf
output "workload_identity_provider" {
  description = "ワークフローの workload_identity_provider に指定する値"
  value       = google_iam_workload_identity_pool_provider.github.name
}

output "service_account_email" {
  description = "ワークフローの service_account に指定する値"
  value       = google_service_account.scc_iac_validator.email
}

apply し、outputの値を控えておきます。

cd bootstrap
terraform init
terraform apply
terraform output workload_identity_provider

workload_identity_provider output は projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID> 形式で返ります。<PROJECT_NUMBER> を手で調べる必要はありません。

この値と service_account_email output を GitHub のリポジトリ Secrets(GCP_WIF_PROVIDER / GCP_SERVICE_ACCOUNT)に登録し、ワークフローからは ${{ secrets.GCP_WIF_PROVIDER }} のように参照します。実機では、この設定で auth ステップがキーレスで通ることを確認しました。

attribute_condition を入れておくと、指定リポジトリ以外の OIDC トークンでは SA を借用できません。

ワークフローを書く

PR で terraform/ 配下が変わったときに走るワークフローです。

permissions には WIF 用の id-token: write が要ります。識別子(組織 ID・プロジェクト番号・プロジェクト ID・SA メール)は public リポジトリに直書きしたくないため、Secrets から渡します(GCP_WIF_PROVIDER / GCP_SERVICE_ACCOUNT / GCP_ORGANIZATION_ID / GCP_PROJECT_ID)。スキャン対象の Terraform も var.project_id にしておき、CI では TF_VAR_project_id を Secret から注入します。

ubuntu-latest の Terraform プリインストールに依存しないように hashicorp/setup-terraform を入れます。terraform_wrapper: false にしておかないと terraform show -json の出力にラッパーのログが混ざります。

.github/workflows/iac-scan.yml
name: IaC Security Scan

on:
  pull_request:
    paths:
      - 'terraform/**'

jobs:
  security-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: 'read'
      id-token: 'write'
    steps:
      - uses: 'actions/checkout@v6'

      - name: 'Authenticate to Google Cloud'
        id: 'auth'
        uses: 'google-github-actions/auth@v3'
        with:
          workload_identity_provider: '${{ secrets.GCP_WIF_PROVIDER }}'
          service_account: '${{ secrets.GCP_SERVICE_ACCOUNT }}'

      - name: 'Set up Terraform'
        uses: 'hashicorp/setup-terraform@v4'
        with:
          terraform_wrapper: false

      - name: 'Generate Terraform Plan'
        env:
          TF_VAR_project_id: '${{ secrets.GCP_PROJECT_ID }}'
        run: |
          cd terraform
          terraform init
          terraform plan -out=tfplan
          terraform show -json tfplan > tf_plan.json

      - name: 'Analyze Code Security'
        id: 'analyze-code-security-scc'
        uses: 'google-github-actions/analyze-code-security-scc@v1'
        with:
          organization_id: '${{ secrets.GCP_ORGANIZATION_ID }}'
          scan_file_ref: './terraform/tf_plan.json'
          iac_type: 'terraform'
          scan_timeout: '3m'
          ignore_violations: false
          failure_criteria: 'Critical:1,High:1,Medium:1,Low:1,Operator:OR'
          fail_silently: false

      - if: |-
          ${{ !cancelled() && steps.analyze-code-security-scc.outputs.iac_scan_result_sarif_path != '' }}
        uses: 'actions/upload-artifact@v7'
        with:
          name: 'sarif'
          path: '${{ steps.analyze-code-security-scc.outputs.iac_scan_result_sarif_path }}'

artifact 出力ステップは公式 README の例に従いました。ifiac_scan_result_sarif_path が空でないときだけ実行するので、Analyze Code Security が違反で失敗した後でも artifact は残ります(job は失敗のまま)。

必須入力は organization_id / scan_file_ref / iac_type の3つです。合否を決めるのは failure_criteria で、重大度別のしきい値と AND/OR を指定します(挙動は後述)。各入力の一覧と意味はAction の READMEにまとまっているので、そちらを参照してください。

出力は iac_scan_resultpassed / failed / error)と iac_scan_result_sarif_path です。SARIF は違反0件でも生成され、その場合 results が空配列になります(後述の「修正して通す」で確認しました)。この path を actions/upload-artifact で artifact 化し、ダウンロードして中身を確認します。

違反入りの PR を作って job を止める

ポスチャーのルールにすべて違反する main.tf(mtu=100、ノード数 2、均一バケットレベルアクセス無効)を使い、PR を出します。

想定どおり job が失敗しました。

Analyze Code Security ステップのログは次のように出ます。

Introduce_policy_violations__m…idation-github-actions_9813900.png

IaC Scanning Action invoked
Successfully read IaC file from: ./terraform/tf_plan.json, IaC type: terraform
Fetching violations report for IaC file
Processing report generation for violations fetched
##[error]google-github-actions/analyze-code-security-scc, reason: failure_criteria was satisfied.
IaC Scanning completed

failure_criteria was satisfied で step が失敗し、job が失敗、PR チェックが赤、という流れです。実行ログには違反の件数や内訳は出ません(出るのは「failure_criteria を満たした」だけ)。件数を確認するには artifact の SARIF を開きます。

artifact の SARIF(results)には5件入っていました。results[].ruleId には SCC 側の識別子(effective なモジュール名やカスタム制約のパス)が入り、message.text に違反内容、properties.assetType にアセット種別が入ります。

severity constraintType 対象
HIGH SECURITY_HEALTH_ANALYTICS_CUSTOM_MODULE(fixedMTU) network(mtu=100)
HIGH ORG_POLICY_CUSTOM(fixedNodeCount) nodePool(count=2)
HIGH ORG_POLICY(uniformBucketLevelAccess) bucket
MEDIUM SECURITY_HEALTH_ANALYTICS_MODULE(BUCKET_POLICY_ONLY_DISABLED) bucket
LOW SECURITY_HEALTH_ANALYTICS_MODULE(BUCKET_LOGGING_DISABLED) bucket

failure_criteria で合否を調整する

ここまでの検証で実機の違反は HIGH×3 / MEDIUM×1 / LOW×1 の5件、デフォルト設定で job が失敗することが分かりました。failure_criteria は「重大度ごとのしきい値」と「Operator(AND/OR)」で合否を決めます。実測した5件に当てはめると、しきい値ごとの合否は次のように整理できます(README の仕様ベース。各パターンを個別に再実行したわけではありません)。

  • デフォルト Critical:1,High:1,Medium:1,Low:1,Operator:OR … どれか1つでもしきい値に達すれば失敗。HIGH が3件あるので失敗
  • High:1,Operator:OR … HIGH が1件以上で失敗。MEDIUM/LOW は無視。失敗のまま
  • Critical:1,Operator:OR … Critical のみで判定。Critical は0件なので、違反は出ていても job は通る(検出はするが落とさない運用)
  • Operator を AND にすると、指定した重大度すべてがしきい値に達したときだけ失敗する

検出はしたいが特定の重大度だけで止めたい、という調整がここでできます。

SARIF を artifact で確認する

検証結果の SARIF は actions/upload-artifact で artifact に保存し、Actions の実行画面からダウンロードして開きます。

Introduce_policy_violations__m…idation-github-actions_9813900.png

違反版では runs[0].results に5件入り、各 result は以下の形でした。

{
  "ruleId": "projects/<PROJECT_NUMBER>/locations/global/effectiveSecurityHealthAnalyticsCustomModules/<ID>",
  "message": { "text": "Asset type: compute.googleapis.com/Network has a violation, next steps: Only create networks whose MTU is 1000." },
  "locations": [
    { "logicalLocations": [{ "fullyQualifiedName": "//compute.googleapis.com/projects/<PROJECT_ID>/global/networks/example-network-1" }] }
  ],
  "properties": { "assetType": "compute.googleapis.com/Network" }
}

locationslogicalLocations(Google Cloud アセットのパス)である点がポイントです。

SCC の IaC 検証は「どのクラウドアセットがどのポリシーに違反したか」を返すので、ソースのファイルや行ではなくアセットを指します。

Terraform を修正して job を通す

修正(mtu=1000、ノード数 3、均一アクセス有効と logging 追加)を入れて再 PR すると、違反が0件になり job が通りました。Analyze Code Security ステップは failure_criteria was satisfied を出さずに IaC Scanning completed で正常終了します。

Introduce_policy_violations__mt…idation-github-actions_ac4cc70.png

SARIF 自体は生成され、results が空配列(0件)になりました(「違反検出時のみ生成」ではありません)。

おわりに

SCC の IaC 検証を、PR トリガーの GitHub Actions に組み込みました。違反があれば job が落ちて PR を止められます。検証結果の SARIF は artifact で確認できます。

ポスチャーを組織に1つ用意しておけば、あとは PR を出すだけで違反を止められるので、ガードレールとして手軽に使えそうです。

この記事をシェアする

関連記事