Security Command Center の IaC 検証で Terraform plan の違反を検出してみた
Security Command Center(SCC)には、デプロイ前の Terraform plan を組織のポリシーと照合する IaC 検証(IaC Validation)機能があります。
gcloud scc iac-validation-reports create にplanファイルを渡すと、違反をレポートで返してくれます。
コマンドの存在は知っていたものの、実際にどんな JSON が返るのかを見たことがなかったので、わざと違反を含む Terraform を書いて試してみました。
以下の公式チュートリアルを参考にしました。
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種類の違反を同時に出せるからです。
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ポリシーすべてに違反する内容を書きます。
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"
}
}
]
}
}
}
読みどころは severity と constraintType です。今回は 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 を修正して、もう一度検証します。
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 = true と logging の追加が、自分のポスチャーの2件だけでなく、既設の組織ポリシーや SHA ビルトイン由来の違反も満たしたからです。
0件にするには、自分が仕込んだルールだけでなく、組織に既設のポリシーや既定で有効な SHA も含めてすべて満たす必要があります。
組織側のルールが多い環境では、想定外の修正まで求められて0件に届かないこともあります。
おわりに
SCC の IaC 検証を実際に動かすと、レポートは severity と constraintType 付きの JSON で返り、デプロイ前のチェックに使えることが確認できました。
次は Cloud Build や GitHub Actions に組み込んで、CI で違反を止めるところまで試してみます。




