diff --git a/gcp-integration-setup/docs/permissions.md b/gcp-integration-setup/docs/permissions.md index c520389..8003479 100644 --- a/gcp-integration-setup/docs/permissions.md +++ b/gcp-integration-setup/docs/permissions.md @@ -45,9 +45,9 @@ Strict allowlist of `*.get` / `*.list` only. | Permission | Purpose | | --- | --- | | `compute.securityPolicies.get/list` | Cloud Armor WAF rule discovery. | -| `accesscontextmanager.accessPolicies.get/list` | VPC Service Controls access policies. | -| `accesscontextmanager.servicePerimeters.get/list` | VPC Service Controls perimeters. | -| `orgpolicy.policies.list` + `orgpolicy.policy.get` | Organisation policy discovery. | +| `accesscontextmanager.accessPolicies.get/list` * | VPC Service Controls access policies. | +| `accesscontextmanager.servicePerimeters.get/list` * | VPC Service Controls perimeters. | +| `orgpolicy.policies.list` + `orgpolicy.policy.get` * | Organisation policy discovery. | | `alloydb.clusters.get/list` + `alloydb.instances.get/list` | AlloyDB topology. | | `file.instances.get/list` | Filestore instance config. | | `redis.instances.get/list` + `memcache.instances.get/list` | Memorystore instance config. | @@ -55,6 +55,8 @@ Strict allowlist of `*.get` / `*.list` only. | `dns.managedZones.get/list` + `dns.resourceRecordSets.list` | Cloud DNS zone + record discovery. | | `apigateway.gateways.get/list` + `apigateway.apis.get/list` + `apigateway.apiconfigs.get/list` | API Gateway topology. | +\* Permissions marked with an asterisk are only includable in **organisation-scoped** custom roles — GCP rejects them in a project-scoped custom role because the underlying resources live at the organisation. They are silently omitted when this module is deployed without `organization_id` (single-project installs); a single-project install simply has no VPC SC or org-policy data to read. + ## What Nullify cannot do | Capability | Granted? | Why not | diff --git a/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf b/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf index cb6060b..8a002eb 100644 --- a/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf +++ b/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf @@ -116,24 +116,13 @@ locals { # custom-role resources actually exists. custom_role_id = local.use_org_custom_role ? google_organization_iam_custom_role.nullify_cloud_connector[0].id : google_project_iam_custom_role.nullify_cloud_connector[0].id - # The full set of permissions Nullify needs above and beyond the predefined - # viewer roles. Strict allowlist of *.get / *.list only — no mutations and - # no data-plane reads. - custom_role_permissions = [ + # Permissions valid in both org- and project-scoped custom roles. Strict + # allowlist of *.get / *.list only — no mutations and no data-plane reads. + custom_role_permissions_common = [ # Cloud Armor security policies (ingress WAF rules). "compute.securityPolicies.get", "compute.securityPolicies.list", - # VPC Service Controls perimeters and access policies. - "accesscontextmanager.accessPolicies.get", - "accesscontextmanager.accessPolicies.list", - "accesscontextmanager.servicePerimeters.get", - "accesscontextmanager.servicePerimeters.list", - - # Organisation policies. - "orgpolicy.policies.list", - "orgpolicy.policy.get", - # AlloyDB clusters + instances. "alloydb.clusters.get", "alloydb.clusters.list", @@ -167,6 +156,22 @@ locals { "apigateway.apiconfigs.get", "apigateway.apiconfigs.list", ] + + # Permissions only includable in an org-scoped custom role. GCP rejects + # these in a project-scoped custom role with "Permission ... is not valid" + # because the underlying resources (VPC SC access policies / perimeters, + # org policies) live at organisation scope. + custom_role_permissions_org_only = [ + # VPC Service Controls perimeters and access policies. + "accesscontextmanager.accessPolicies.get", + "accesscontextmanager.accessPolicies.list", + "accesscontextmanager.servicePerimeters.get", + "accesscontextmanager.servicePerimeters.list", + + # Organisation policies. + "orgpolicy.policies.list", + "orgpolicy.policy.get", + ] } # --------------------------------------------------------------------------- @@ -193,7 +198,7 @@ resource "google_organization_iam_custom_role" "nullify_cloud_connector" { title = "Nullify Cloud Connector (read-only)" description = "Read-only access to security-relevant config Nullify needs that is not covered by predefined viewer roles." stage = "GA" - permissions = local.custom_role_permissions + permissions = concat(local.custom_role_permissions_common, local.custom_role_permissions_org_only) } resource "google_project_iam_custom_role" "nullify_cloud_connector" { @@ -203,7 +208,13 @@ resource "google_project_iam_custom_role" "nullify_cloud_connector" { title = "Nullify Cloud Connector (read-only)" description = "Read-only access to security-relevant config Nullify needs that is not covered by predefined viewer roles." stage = "GA" - permissions = local.custom_role_permissions + permissions = local.custom_role_permissions_common + + # iam.googleapis.com must be enabled before a custom role can be created. + # Other resources in this module already declare this dependency; the + # project-scoped custom role was the odd one out and could race the API + # enable on a fresh project. + depends_on = [google_project_service.required] } # ---------------------------------------------------------------------------