Security Command Center の IaC 検証で Terraform plan の違反を検出してみた

Security Command Center の IaC 検証で Terraform plan の違反を検出してみた

2026.06.03

Security Command Center(SCC)には、デプロイ前の Terraform plan を組織のポリシーと照合する IaC 検証(IaC Validation)機能があります。

gcloud scc iac-validation-reports create にplanファイルを渡すと、違反をレポートで返してくれます。

コマンドの存在は知っていたものの、実際にどんな JSON が返るのかを見たことがなかったので、わざと違反を含む Terraform を書いて試してみました。

以下の公式チュートリアルを参考にしました。

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

IaC 検証の仕組み

IaC 検証は、Terraform plan を JSON にしたファイルを入力に取り、組織にデプロイ済みのセキュリティポスチャーと照合します。

照合対象のポスチャーには、種類の違うルールをまとめて入れられます。今回登場したのは3種類です。

  • 組織ポリシー(Organization Policy): リソースの作成を制限する予防的なルール
  • Security Health Analytics(SHA)のビルトインモジュール: Google 既製の検出ルール
  • SHA のカスタムモジュール: 自分で条件(CEL 式)を書く検出ルール

前提とハマりどころ(プロジェクトレベルでは使えない)

IaC 検証を使うには、SCC が Premium または Enterprise で、かつ組織レベルで有効化されている必要があります。

SCC Premium はプロジェクト単位でも有効化できますが、その場合 Security Posture が使えません。

公式ドキュメントのプロジェクトレベル有効化の制限にも、Security posture management はプロジェクトレベル有効化では使えないと明記されています。

検証の流れ

以降のコマンドやファイルに出てくる <...> は、環境に合わせて置き換える値です。<ORGANIZATION_ID> は組織 ID、<PROJECT_ID> はプロジェクト ID、<PROJECT_NUMBER> はプロジェクト番号を指します。

create と deploy が別ステップなのがポイントです。作っただけでは照合対象にならず、デプロイして初めて有効なポリシーになります。

違反を仕込んだポスチャーを作る

公式チュートリアルの example-standard.yaml をそのまま使います。4つのポリシーが入っていますが、仕組みとしては前述の3種類です。SHA ビルトインだけ2つあるのは、1つのバケットで2種類の違反を同時に出せるからです。

example-standard.yaml
name: organizations/<ORGANIZATION_ID>/locations/global/postures/example-standard
state: ACTIVE
policySets:
  - policies:
      - constraint:
          orgPolicyConstraintCustom:
            customConstraint:
              actionType: ALLOW
              condition: "resource.initialNodeCount == 3"
              description: Set initial node count to be exactly 3.
              displayName: fixedNodeCount
              methodTypes:
                - CREATE
              name: organizations/<ORGANIZATION_ID>/customConstraints/custom.fixedNodeCount
              resourceTypes:
                - container.googleapis.com/NodePool
            policyRules:
              - enforce: true
        policyId: fixedNodeCount
      - constraint:
          securityHealthAnalyticsCustomModule:
            config:
              customOutput: {}
              description: Set MTU for a network to be exactly 1000.
              predicate:
                expression: "!(resource.mtu == 1000)"
              recommendation: Only create networks whose MTU is 1000.
              resourceSelector:
                resourceTypes:
                  - compute.googleapis.com/Network
              severity: HIGH
            displayName: fixedMTU
            moduleEnablementState: ENABLED
        policyId: fixedMTU
      - constraint:
          securityHealthAnalyticsModule:
            moduleEnablementState: ENABLED
            moduleName: BUCKET_POLICY_ONLY_DISABLED
        policyId: bucket_policy_only_disabled
      - constraint:
          securityHealthAnalyticsModule:
            moduleEnablementState: ENABLED
            moduleName: BUCKET_LOGGING_DISABLED
        policyId: bucket_logging_disabled
    policySetId: policySet1

組織 ID とプロジェクト番号を控えておきます。

gcloud organizations list
gcloud projects describe <PROJECT_ID> --format="value(projectNumber)"

ポスチャーを作成します。

gcloud scc postures create \
  organizations/<ORGANIZATION_ID>/locations/global/postures/example-standard \
  --posture-from-file=example-standard.yaml \
  --format="value(revisionId)"

出力されるリビジョン ID は次のデプロイで使うので控えておきます。

ポスチャーをプロジェクトにデプロイする

--target-resource に対象プロジェクトを指定してデプロイします。番号で指定する点に注意してください。

gcloud scc posture-deployments create \
  organizations/<ORGANIZATION_ID>/locations/global/postureDeployments/example-standard \
  --posture-name=organizations/<ORGANIZATION_ID>/locations/global/postures/example-standard \
  --posture-revision-id="<POSTURE_REVISION_ID>" \
  --target-resource=projects/<PROJECT_NUMBER>

state: ACTIVE が返ればデプロイ完了です。組織ポリシーや SHA カスタムモジュールの反映には少し時間がかかることがあります。

ここで1つ確認したことがあります。デプロイする前に検証を回したらどうなるか、です。試したところ違反は0件ではなく3件出ました。

  • storage.uniformBucketLevelAccess(HIGH、組織ポリシー)
  • BUCKET_POLICY_ONLY_DISABLED(MEDIUM、SHA ビルトイン)
  • BUCKET_LOGGING_DISABLED(LOW、SHA ビルトイン)

gcloud scc posture-deployments list で他のポスチャーが無いこと、gcloud org-policies list --organization=<ORGANIZATION_ID>storage.uniformBucketLevelAccess が組織に既設なことを確認しました。SHA のビルトインは既定で有効になっています。組織ポリシーはこの組織に元から設定されていました。

つまり「ポスチャーをデプロイし忘れると違反0件になる」という単純な話ではありません。正確には、カスタムルールはデプロイしないと出ませんが、SHA ビルトインと既設の組織ポリシーはデプロイの有無に関係なく検出されます。デプロイで増えるのは自分が作ったカスタム2件だけです。

違反する Terraform を書く

ポスチャーの4ポリシーすべてに違反する内容を書きます。

main.tf
resource "google_compute_network" "example_network" {
  name                            = "example-network-1"
  delete_default_routes_on_create = false
  auto_create_subnetworks         = false
  routing_mode                    = "REGIONAL"
  mtu                             = 100
  project                         = "<PROJECT_ID>"
}

resource "google_container_node_pool" "example_node_pool" {
  name               = "example-node-pool-1"
  cluster            = "example-cluster-1"
  project            = "<PROJECT_ID>"
  initial_node_count = 2
  node_config {
    preemptible  = true
    machine_type = "e2-medium"
  }
}

resource "google_storage_bucket" "example_bucket" {
  name                        = "example-bucket-1"
  location                    = "EU"
  force_destroy               = true
  project                     = "<PROJECT_ID>"
  uniform_bucket_level_access = false
}

MTU が 100、ノード数が 2、均一バケットレベルアクセスが無効でロギングブロック無し、と全部わざと外しています。

plan を JSON にして検証する

IaC 検証に渡すのは .tf のソースではなく、plan を JSON にしたファイルです。リソースの確定後の値を見たいので、planを通す必要があります。

terraform init
terraform plan -out main.plan
terraform show -json main.plan > mainplan.json

この JSON を検証コマンドに渡します。なお securityposture.googleapis.com が未有効だと初回実行時に有効化を聞かれます。先に有効化しておくと止まりません。

gcloud services enable \
  securityposture.googleapis.com \
  securitycentermanagement.googleapis.com \
  --project=<PROJECT_ID>

gcloud scc iac-validation-reports create \
  organizations/<ORGANIZATION_ID>/locations/global \
  --tf-plan-file=mainplan.json

ただし生の出力はかなり冗長です。違反したアセットの全フィールドが proto-text で入るため、今回は99行になりました。

Waiting for operation [organizations/<ORGANIZATION_ID>/...] to complete...
................................................done.
'@type': type.googleapis.com/google.cloud.securityposture.v1.Report
createTime: '2026-06-03T04:53:49.658005681Z'
iacValidationReport:
  note: IaC validation is limited to certain asset types and policies. ...
  violations:
  - assetId: //compute.googleapis.com/projects/<PROJECT_ID>/global/networks/example-network-1
    nextSteps: Only create networks whose MTU is 1000.
    policyId: projects/<PROJECT_NUMBER>/locations/global/effectiveSecurityHealthAnalyticsCustomModules/14433188154219679863
    severity: HIGH
    violatedAsset:
      asset: resource:{version:"beta" ... fields:{key:"mtu" value:{number_value:100}} ...}
    # ...省略...

レポート JSON を読む

レポート本体だけを見やすく取り出すには --format で絞ります。

gcloud scc iac-validation-reports create \
  organizations/<ORGANIZATION_ID>/locations/global \
  --tf-plan-file=mainplan.json \
  --format="json(response.iacValidationReport)"

返ってきた違反は、想定の4件ではなく5件でした。冗長な violatedAsset.asset は省略しています。

{
  "response": {
    "iacValidationReport": {
      "note": "IaC validation is limited to certain asset types and policies. ...",
      "violations": [
        {
          "assetId": "//compute.googleapis.com/projects/<PROJECT_ID>/global/networks/example-network-1",
          "nextSteps": "Only create networks whose MTU is 1000.",
          "policyId": "projects/<PROJECT_NUMBER>/locations/global/effectiveSecurityHealthAnalyticsCustomModules/14433188154219679863",
          "severity": "HIGH",
          "violatedPolicy": {
            "constraintType": "SECURITY_HEALTH_ANALYTICS_CUSTOM_MODULE",
            "description": "Set MTU for a network to be exactly 1000."
          }
        },
        {
          "assetId": "//container.googleapis.com/projects/<PROJECT_ID>/locations/placeholder-phG2hydz/clusters/example-cluster-1/nodePools/example-node-pool-1",
          "policyId": "projects/<PROJECT_NUMBER>/policies/custom.fixedNodeCount...",
          "severity": "HIGH",
          "violatedPolicy": {
            "constraintType": "ORG_POLICY_CUSTOM"
          }
        },
        {
          "assetId": "//storage.googleapis.com/example-bucket-1",
          "nextSteps": "In the bucket configuration access control should be uniform instead of fine grained",
          "policyId": "organizations/<ORGANIZATION_ID>/policies/storage.uniformBucketLevelAccess",
          "severity": "HIGH",
          "violatedPolicy": {
            "constraintType": "ORG_POLICY"
          }
        },
        {
          "assetId": "//storage.googleapis.com/example-bucket-1",
          "nextSteps": "Logging should be Enabled for Cloud storage buckets",
          "policyId": "projects/<PROJECT_ID>/SECURITY_HEALTH_ANALYTICS/BUCKET_LOGGING_DISABLED",
          "severity": "LOW",
          "violatedPolicy": {
            "constraintType": "SECURITY_HEALTH_ANALYTICS_MODULE"
          }
        },
        {
          "assetId": "//storage.googleapis.com/example-bucket-1",
          "nextSteps": "Bucket Policy Only should be Enabled",
          "policyId": "projects/<PROJECT_ID>/SECURITY_HEALTH_ANALYTICS/BUCKET_POLICY_ONLY_DISABLED",
          "severity": "MEDIUM",
          "violatedPolicy": {
            "constraintType": "SECURITY_HEALTH_ANALYTICS_MODULE"
          }
        }
      ]
    }
  }
}

読みどころは severityconstraintType です。今回は constraintType が4種類に分かれました。

asset severity constraintType 由来
network(mtu=100) HIGH SECURITY_HEALTH_ANALYTICS_CUSTOM_MODULE 今回のポスチャー(fixedMTU)
nodePool(count=2) HIGH ORG_POLICY_CUSTOM 今回のポスチャー(fixedNodeCount)
bucket(均一アクセス) HIGH ORG_POLICY 組織に既設のポリシー
bucket(ロギング) LOW SECURITY_HEALTH_ANALYTICS_MODULE SHA ビルトイン
bucket(policy only) MEDIUM SECURITY_HEALTH_ANALYTICS_MODULE SHA ビルトイン

4件想定が5件になった理由は、バケットの均一アクセスが「既設の組織ポリシー」と「SHA の BUCKET_POLICY_ONLY_DISABLED」の両方から二重に検出されるためです。

検出される件数は組織の状態に左右されます。今回デプロイしたポスチャーだけでなく、組織に元から効いているポリシーや 既定で有効な SHA ビルトインも一緒に出る、という点は実際に動かして初めて分かりました。

違反を直して再検証する

main.tf を修正して、もう一度検証します。

main.tf
  resource "google_compute_network" "example_network" {
    name                            = "example-network-1"
    routing_mode                    = "REGIONAL"
-   mtu                             = 100
+   mtu                             = 1000
    project                         = "<PROJECT_ID>"
  }

  resource "google_container_node_pool" "example_node_pool" {
    name               = "example-node-pool-1"
-   initial_node_count = 2
+   initial_node_count = 3
    project            = "<PROJECT_ID>"
    node_config {
      preemptible  = true
      machine_type = "e2-medium"
    }
  }

  resource "google_storage_bucket" "example_bucket" {
    name                        = "example-bucket-1"
    location                    = "EU"
-   uniform_bucket_level_access = false
+   uniform_bucket_level_access = true
+   logging {
+     log_bucket        = "my-unique-logging-bucket"
+     log_object_prefix = "tf-logs/"
+   }
    project                     = "<PROJECT_ID>"
  }

再度 plan を JSON 化して検証すると、違反は0件になりました。0件のときは violations 配列自体が無くなり、note だけが返ります。

{ "response": { "iacValidationReport": { "note": "IaC validation is limited to certain asset types and policies. ..." } } }

1つ補足です。今回5件すべてが0件になったのは、uniform_bucket_level_access = truelogging の追加が、自分のポスチャーの2件だけでなく、既設の組織ポリシーや SHA ビルトイン由来の違反も満たしたからです。

0件にするには、自分が仕込んだルールだけでなく、組織に既設のポリシーや既定で有効な SHA も含めてすべて満たす必要があります。

組織側のルールが多い環境では、想定外の修正まで求められて0件に届かないこともあります。

おわりに

SCC の IaC 検証を実際に動かすと、レポートは severityconstraintType 付きの JSON で返り、デプロイ前のチェックに使えることが確認できました。

次は Cloud Build や GitHub Actions に組み込んで、CI で違反を止めるところまで試してみます。

この記事をシェアする

関連記事