From 261a325e0a1ed40ccc50df7983c634fb50a64b2d Mon Sep 17 00:00:00 2001 From: Tim Thacker Date: Thu, 30 Apr 2026 21:49:27 +1000 Subject: [PATCH] feat(gcp): grant config-only perms for storage, secrets, bigquery, cloudbuild, batch, workflows, firestore, vertex, scc Closes the gap between what the Nullify cloud scanner enumerates and what the cloud connector module grants. Adds 23 permissions across 9 services to the existing nullifyCloudConnector custom role; no new predefined role bindings. Every permission is a *.list / *.get / *.getMetadata on configuration resources. Data-plane perms are explicitly excluded: - storage.objects.* (no object reads) - secretmanager.versions.access (no secret payloads) - bigquery.tables.getData (no row reads) - bigquery.jobs.create (no query execution / billing) - workflows.executions.* (no execution payloads) - workflows.stepEntries.* (no step input/output reads) - datastore.entities.* (no document reads) - aiplatform.endpoints.predict (no inference) - securitycenter.findings.* (no finding content) The predefined viewer roles for Workflows, Firestore (datastore.viewer), Vertex AI, and SCC findings are deliberately not used because each one includes data-plane permissions that violate the connector's config-only contract. scripts/install.sh CUSTOM_ROLE_PERMISSIONS string is kept in sync. docs/permissions.md is extended with the new permissions and the new "What Nullify cannot do" rows. Co-Authored-By: Claude Opus 4.7 (1M context) --- gcp-integration-setup/docs/permissions.md | 22 ++++++- gcp-integration-setup/scripts/install.sh | 2 +- gcp-integration-setup/terraform/README.md | 12 ++-- .../modules/nullify-gcp-integration/main.tf | 62 ++++++++++++++++++- 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/gcp-integration-setup/docs/permissions.md b/gcp-integration-setup/docs/permissions.md index c520389..9a17e27 100644 --- a/gcp-integration-setup/docs/permissions.md +++ b/gcp-integration-setup/docs/permissions.md @@ -54,14 +54,30 @@ Strict allowlist of `*.get` / `*.list` only. | `artifactregistry.repositories.get/list` | Artifact Registry repo metadata (no image content). | | `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. | +| `storage.buckets.get/list` + `storage.buckets.getIamPolicy` | Cloud Storage bucket settings + bucket-level IAM. No `storage.objects.*`. | +| `secretmanager.secrets.get/list` | Secret Manager: secret name, labels, replication policy, rotation config. No `secretmanager.versions.access` (payloads). | +| `bigquery.datasets.get/list` + `bigquery.tables.get/list` + `bigquery.routines.get/list` | BigQuery dataset/table/routine schema + IAM. No `bigquery.tables.getData` (rows) and no `bigquery.jobs.create` (no query execution / billing). | +| `cloudbuild.buildTriggers.get/list` | Cloud Build trigger config (repo binding, file filter, substitutions). No build logs or artifacts. | +| `batch.jobs.get/list` | Cloud Batch job spec. No task logs or output artifacts. | +| `workflows.workflows.get/list` | Cloud Workflows: workflow definitions only. **Not** `workflows.executions.*` or `workflows.stepEntries.*` — execution arguments and step inputs/outputs are runtime data. | +| `datastore.databases.list` + `datastore.databases.getMetadata` | Firestore database list + metadata. **Not** `datastore.entities.*` — document contents are runtime data. (Firestore in Native and Datastore modes share the `datastore.*` IAM family.) | +| `aiplatform.endpoints.get/list` | Vertex AI endpoint deployment config. No `aiplatform.endpoints.predict` (inference) and no model/dataset/featurestore reads. | +| `securitycenter.sources.get/list` | Security Command Center source config (which detection sources are wired up). **Not** `securitycenter.findings.*` or `securitycenter.assets.*` — finding contents are runtime data. Org-scope only; harmless no-op at project scope. | ## What Nullify cannot do | Capability | Granted? | Why not | | --- | --- | --- | -| Read object data from Cloud Storage | No | `roles/storage.objectViewer` is intentionally **not** granted. We only see bucket metadata. | -| Read secret payloads from Secret Manager | No | `roles/secretmanager.secretAccessor` is intentionally **not** granted. We only see secret names and metadata. | -| Read BigQuery table rows | No | `roles/bigquery.dataViewer` is intentionally **not** granted. We only see dataset metadata. | +| Read object data from Cloud Storage | No | `roles/storage.objectViewer` is intentionally **not** granted. We only see bucket settings + bucket IAM. | +| Read secret payloads from Secret Manager | No | `roles/secretmanager.secretAccessor` is intentionally **not** granted. We only see secret names, labels, replication policy. | +| Read BigQuery table rows | No | `roles/bigquery.dataViewer` is intentionally **not** granted. We only see dataset/table schema + IAM. | +| Run BigQuery queries | No | `bigquery.jobs.create` is **not** granted. No query execution and no billable jobs. | +| Read Workflow execution payloads | No | `workflows.executions.*` and `workflows.stepEntries.*` are **not** granted. We only see workflow definitions, never the inputs/outputs of an execution. The predefined `roles/workflows.viewer` is **not** used because it would expose execution payloads. | +| Read Firestore document contents | No | `datastore.entities.*` is **not** granted. We only see the database list. The predefined `roles/datastore.viewer` is **not** used because it grants document reads. | +| Run Vertex AI inference | No | `aiplatform.endpoints.predict` and `computeTokens` are **not** granted. We see endpoint config only — not models, datasets, or featurestores. The broader `roles/aiplatform.viewer` is **not** used. | +| Read SCC findings | No | `securitycenter.findings.*` and `securitycenter.assets.*` are **not** granted. We only see which detection sources are configured. | +| Read Cloud Build logs or artifacts | No | Trigger config only. No build logs, artifacts, or source contents. | +| Read Cloud Batch task logs | No | Job spec only. No task logs or output artifacts. | | Modify your environment | No | Every role above is read-only. There are no write/admin roles. | | Run code or workloads | No | No `roles/run.invoker`, `roles/cloudfunctions.invoker` etc. | diff --git a/gcp-integration-setup/scripts/install.sh b/gcp-integration-setup/scripts/install.sh index cf0d9e8..b7af09e 100755 --- a/gcp-integration-setup/scripts/install.sh +++ b/gcp-integration-setup/scripts/install.sh @@ -104,7 +104,7 @@ CUSTOM_ROLE_TITLE="Nullify Cloud Connector (read-only)" CUSTOM_ROLE_DESCRIPTION="Read-only access to security-relevant config Nullify needs that is not covered by predefined viewer roles." # shellcheck disable=SC2089 -CUSTOM_ROLE_PERMISSIONS="compute.securityPolicies.get,compute.securityPolicies.list,accesscontextmanager.accessPolicies.get,accesscontextmanager.accessPolicies.list,accesscontextmanager.servicePerimeters.get,accesscontextmanager.servicePerimeters.list,orgpolicy.policies.list,orgpolicy.policy.get,alloydb.clusters.get,alloydb.clusters.list,alloydb.instances.get,alloydb.instances.list,file.instances.get,file.instances.list,redis.instances.get,redis.instances.list,memcache.instances.get,memcache.instances.list,artifactregistry.repositories.get,artifactregistry.repositories.list,dns.managedZones.get,dns.managedZones.list,dns.resourceRecordSets.list,apigateway.gateways.get,apigateway.gateways.list,apigateway.apis.get,apigateway.apis.list,apigateway.apiconfigs.get,apigateway.apiconfigs.list" +CUSTOM_ROLE_PERMISSIONS="compute.securityPolicies.get,compute.securityPolicies.list,accesscontextmanager.accessPolicies.get,accesscontextmanager.accessPolicies.list,accesscontextmanager.servicePerimeters.get,accesscontextmanager.servicePerimeters.list,orgpolicy.policies.list,orgpolicy.policy.get,alloydb.clusters.get,alloydb.clusters.list,alloydb.instances.get,alloydb.instances.list,file.instances.get,file.instances.list,redis.instances.get,redis.instances.list,memcache.instances.get,memcache.instances.list,artifactregistry.repositories.get,artifactregistry.repositories.list,dns.managedZones.get,dns.managedZones.list,dns.resourceRecordSets.list,apigateway.gateways.get,apigateway.gateways.list,apigateway.apis.get,apigateway.apis.list,apigateway.apiconfigs.get,apigateway.apiconfigs.list,storage.buckets.get,storage.buckets.list,storage.buckets.getIamPolicy,secretmanager.secrets.get,secretmanager.secrets.list,bigquery.datasets.get,bigquery.datasets.list,bigquery.tables.get,bigquery.tables.list,bigquery.routines.get,bigquery.routines.list,cloudbuild.buildTriggers.get,cloudbuild.buildTriggers.list,batch.jobs.get,batch.jobs.list,workflows.workflows.get,workflows.workflows.list,datastore.databases.getMetadata,datastore.databases.list,aiplatform.endpoints.get,aiplatform.endpoints.list,securitycenter.sources.get,securitycenter.sources.list" echo "==> Creating organisation custom role ${CUSTOM_ROLE_ID}" if gcloud iam roles describe "${CUSTOM_ROLE_ID}" --organization="${NULLIFY_ORG_ID}" >/dev/null 2>&1; then diff --git a/gcp-integration-setup/terraform/README.md b/gcp-integration-setup/terraform/README.md index 25fabea..8dc33f1 100644 --- a/gcp-integration-setup/terraform/README.md +++ b/gcp-integration-setup/terraform/README.md @@ -11,10 +11,14 @@ secrets. Nullify's OIDC issuer URL, with an `attribute_condition` pinned to your specific Nullify tenant id. - A custom role with read-only permissions on the long-tail services that - don't have a predefined viewer role (Cloud Armor, VPC Service Controls, - AlloyDB, Filestore, Memorystore, Cloud DNS, API Gateway, Artifact - Registry). Defined at the org for `scope = "organization" | "folder"`, - at the project for `scope = "projects"`. + don't have a suitable predefined viewer role (Cloud Armor, VPC Service + Controls, AlloyDB, Filestore, Memorystore, Cloud DNS, API Gateway, + Artifact Registry, Cloud Storage, Secret Manager, BigQuery, Cloud Build, + Cloud Batch, Cloud Workflows, Firestore, Vertex AI, Security Command + Center). Defined at the org for `scope = "organization" | "folder"`, + at the project for `scope = "projects"`. Strict allowlist of `*.get` / + `*.list` only — no data-plane reads (no object/secret/row/document + contents, no execution payloads, no inference, no findings). - IAM bindings granting the Nullify service account a curated set of predefined viewer roles plus the custom role above. Bound at organisation scope by default; folder and per-project scopes are also supported. 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..99513ec 100644 --- a/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf +++ b/gcp-integration-setup/terraform/modules/nullify-gcp-integration/main.tf @@ -18,7 +18,11 @@ # and network-topology metadata. There are NO data-plane permissions: # - storage: bucket metadata only, never object data # - secret manager: secret metadata only, never secret payloads -# - bigquery: dataset metadata only, never table rows +# - bigquery: schema/IAM only, never table rows +# - workflows: workflow definitions only, never execution inputs/outputs +# - firestore: database list only, never document contents +# - vertex ai: endpoint config only, never inference inputs/outputs +# - security command center: source config only, never finding contents # # See ../../docs/permissions.md for the full permission list + rationale. @@ -166,6 +170,62 @@ locals { "apigateway.apis.list", "apigateway.apiconfigs.get", "apigateway.apiconfigs.list", + + # Cloud Storage bucket settings + bucket-level IAM (no object data — + # storage.objects.* is intentionally not granted). + "storage.buckets.get", + "storage.buckets.list", + "storage.buckets.getIamPolicy", + + # Secret Manager: secret name, labels, replication policy, rotation + # config (no secretmanager.versions.access — payloads are never read). + "secretmanager.secrets.get", + "secretmanager.secrets.list", + + # BigQuery dataset/table/routine schema + IAM. No bigquery.tables.getData + # (row data) and no bigquery.jobs.create (no query execution / billing). + "bigquery.datasets.get", + "bigquery.datasets.list", + "bigquery.tables.get", + "bigquery.tables.list", + "bigquery.routines.get", + "bigquery.routines.list", + + # Cloud Build trigger config (repo binding, file filter, substitutions). + # No build logs, artifacts, or source contents. + "cloudbuild.buildTriggers.get", + "cloudbuild.buildTriggers.list", + + # Cloud Batch job spec. No task logs or output artifacts. + "batch.jobs.get", + "batch.jobs.list", + + # Cloud Workflows: workflow definitions only. workflows.executions.* and + # workflows.stepEntries.* are intentionally NOT granted — execution + # arguments and step inputs/outputs are runtime data. + "workflows.workflows.get", + "workflows.workflows.list", + + # Firestore: database list + metadata. datastore.entities.* is + # intentionally NOT granted — document contents are runtime data. + # (Note: GCP uses the datastore.* IAM family for Firestore in both + # Native and Datastore modes; firestore.* may be added later as GCP + # migrates the IAM surface.) + "datastore.databases.getMetadata", + "datastore.databases.list", + + # Vertex AI: endpoint deployment config only. aiplatform.endpoints.predict + # / computeTokens and all dataset/featurestore/model perms are + # intentionally NOT granted. + "aiplatform.endpoints.get", + "aiplatform.endpoints.list", + + # Security Command Center: source config (which detection sources are + # wired up). securitycenter.findings.* and securitycenter.assets.* are + # intentionally NOT granted — finding contents are runtime data. + # Org-scope only — at project scope these calls return empty harmlessly. + "securitycenter.sources.get", + "securitycenter.sources.list", ] }