feat(gcp): GCP cloud connector terraform module + gcloud installer#40
Conversation
Adds a read-only GCP integration option matching the AWS pattern. Trust
model is Workload Identity Federation only — no service account JSON keys,
no long-lived secrets. Customer can revoke at any time via terraform
destroy or the uninstall.sh script.
Layout (mirrors aws-integration-setup):
gcp-integration-setup/
terraform/
main.tf, variables.tf, outputs.tf, providers.tf, versions.tf
terraform.tfvars.example
README.md
modules/nullify-gcp-integration/
main.tf - WIF pool + AWS provider, SA, custom role, bindings
variables.tf, outputs.tf, versions.tf
examples/
organization/main.tf - org-wide install
single-project/main.tf - per-project install
scripts/
install.sh - idempotent gcloud one-shot installer
uninstall.sh - revocation counterpart
docs/
permissions.md - every role + custom permission documented with rationale
Permissions are intentionally read-only and limited to service-config and
network-topology metadata. Explicit non-grants:
- storage.objectViewer (object data)
- secretmanager.secretAccessor (secret payloads)
- bigquery.dataViewer (table rows)
- any write/admin role
Predefined viewer roles: cloudasset.viewer, iam.securityReviewer, viewer,
compute.viewer, container.clusterViewer, cloudsql.viewer, spanner.viewer,
cloudkms.viewer, logging.viewer, run.viewer, cloudfunctions.viewer,
appengine.appViewer, dataproc.viewer, dataflow.viewer, pubsub.viewer.
Custom role nullifyCloudConnector covers the long tail (Cloud Armor,
VPC Service Controls, AlloyDB, Filestore, Memorystore, Cloud DNS,
API Gateway, Artifact Registry metadata) with strict *.get/*.list allowlist.
The Workload Identity provider is configured with an attribute_condition
restricting trust to a single Nullify AWS IAM role. Even if the WIF pool is
exposed, only the exact Nullify principal can mint a token. This matches the
external_account credential JSON the Nullify backend synthesises in
hyperdrive/pkg/cloudintegrations/gcp/auth.go.
Module supports two scoping modes via the `scope` variable:
- "organization" (recommended): bind roles at the org level
- "projects": bind roles only on the project_ids list (POC mode)
After terraform apply the customer pastes service_account_email and
workload_identity_provider into the Nullify console -> Settings -> Cloud
Integrations -> GCP and clicks Verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Did a read-through of the module and the gcloud installer. A few things I think are worth addressing before this comes out of draft — happy to open follow-up PRs for any of them. Scope coverageThe PR offers Custom role scope mismatch
One fix is to define the role with
|
Larger pass through vik-nullify's review of the GCP integration module.
## Custom-role binding bug (apply-time failure)
`google_project_iam_custom_role` only assigns within its defining project,
so binding it on the organisation (`google_organization_iam_member.custom`)
or on a sibling project (`google_project_iam_member.custom` when
project_ids != [host_project_id]) failed at apply time. The
`examples/single-project` example tripped this today.
Fix: introduce `google_organization_iam_custom_role.nullify_cloud_connector`
and select between the two variants via `local.use_org_custom_role` based
on whether organization_id is set. Bindings now reference
`local.custom_role_id`. The single-project example is rewired so
host_project_id and project_ids match (the only configuration that works
without an organization_id), and a new module-level
`terraform_data.input_validation` precondition rejects multi-project
installs that omit organization_id at plan time rather than blowing up
mid-apply.
## Folder scope
Added `scope = "folder"` with a `folder_id` variable, matching
`google_folder_iam_member` bindings (predefined + custom), and a new
`examples/folder/` example. Org_id is required for folder scope so the
custom role can be defined at the organisation and assigned on the folder.
## install.sh ↔ Terraform parity
The shell installer now mirrors the Terraform module's security posture:
1. **`--attribute-condition`** is now passed to
`gcloud iam workload-identity-pools providers create-aws`. Previously
the WIF provider trusted any principal in the Nullify AWS account
rather than the single role Terraform pins.
2. **Custom role created and bound** at the organisation, mirroring
`local.custom_role_permissions`. Previously the gcloud path silently
missed Cloud Armor / VPC SC / orgpolicy / AlloyDB / Filestore /
Memorystore / Artifact Registry / Cloud DNS / API Gateway permissions.
3. **`uninstall.sh`** now removes the custom-role binding and deletes
the org-level custom role.
4. **`NULLIFY_TENANT_EXTERNAL_ID`** is no longer required by install.sh
because the variable was dead code in the Terraform module too (see
below).
5. The script header documents that install.sh is org-scope-only and
points folder/project-scope users at Terraform.
## `tenant_external_id` / `labels` were dead code
The module declared `tenant_external_id` and `labels`, merged them into
`local.common_labels`, and never referenced `common_labels` anywhere else.
GCP IAM resources don't expose a `labels` argument and no resource block
in the module set one, so the variables had zero on-cluster effect. README
and module docstrings claimed otherwise. Removed the merge, the locals
block, and both variables. The examples and tfvars.example are updated.
The `customer_name` variable is kept (still useful for support correlation)
but its description now reflects that it isn't actually a resource label.
## AWS role-path regex
`replace(var.nullify_aws_principal_arn, "/^arn:aws:iam::[0-9]+:role\\//", "")`
left a `path/RoleName` if the principal were ever issued under a path,
which would never match the assumed-role assertion (assumed-role ARNs
contain only the friendly name). Replaced with a path-tolerant
`element(reverse(split("/", ...)), 0)` extraction in the new
`local.nullify_aws_role_name`, used in both the WIF provider's
`attribute_condition` and the service account's
`workloadIdentityUser` binding. install.sh already used `${var##*/}`
which is equivalent.
## Cleanup
- `google-beta` provider declaration and `providers.tf` block dropped
from the root and the module — no resource was using it.
- `data "google_project" "host"` moved out of `outputs.tf` into a new
`data.tf` so reviewers don't have to find a data source at the bottom
of an outputs file.
- `next_steps` output wording updated to read sensibly at org / folder
/ project scope (was hardcoded "green check next to every project").
- Top-level README gains a GCP quick-start section pointing at the
module's README.
## CI
New `.github/workflows/terraform-validate.yml` runs
`terraform fmt -check`, `terraform init -backend=false`, and
`terraform validate` against the root module and every example
(`gcp-integration-setup/terraform`, the three examples, and the
existing AWS modules), plus `shellcheck` on install.sh / uninstall.sh.
Targets the same set of paths the customer would actually apply, so a
regression of the org-binding bug above (or any future structural
mistake) gets caught at PR time rather than at customer apply time.
Verified locally:
- `terraform fmt -check -recursive gcp-integration-setup/` clean
- `terraform validate` passes for the root module + all 3 examples
- `shellcheck` clean for install.sh and uninstall.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Adds a read-only GCP integration option matching the existing AWS pattern. Trust model is Workload Identity Federation only — no service account JSON keys, no long-lived secrets. Customer can revoke at any time via `terraform destroy` or `uninstall.sh`.
This is the customer-facing IaC for the larger "complete the GCP integration" effort happening in the nullify-7 monorepo (separate PR).
What's included
```
gcp-integration-setup/
terraform/
main.tf, variables.tf, outputs.tf, providers.tf, versions.tf
terraform.tfvars.example
README.md
modules/nullify-gcp-integration/
main.tf — WIF pool + AWS provider, SA, custom role, bindings
variables.tf, outputs.tf, versions.tf
examples/
organization/main.tf — org-wide install
single-project/main.tf — per-project install
scripts/
install.sh — idempotent gcloud one-shot installer
uninstall.sh — revocation counterpart
docs/
permissions.md — every role + custom permission documented with rationale
```
Permission model
Read-only, service-config and network-topology metadata only. Explicit non-grants:
Predefined viewer roles granted: `cloudasset.viewer`, `iam.securityReviewer`, `viewer`, `compute.viewer`, `container.clusterViewer`, `cloudsql.viewer`, `spanner.viewer`, `cloudkms.viewer`, `logging.viewer`, `run.viewer`, `cloudfunctions.viewer`, `appengine.appViewer`, `dataproc.viewer`, `dataflow.viewer`, `pubsub.viewer`.
Custom role `nullifyCloudConnector` covers the long tail (Cloud Armor, VPC Service Controls, AlloyDB, Filestore, Memorystore, Cloud DNS, API Gateway, Artifact Registry metadata) with strict `.get`/`.list` allowlist.
Full justification per role/permission lives in `gcp-integration-setup/docs/permissions.md`.
WIF trust pinning
The Workload Identity provider's `attribute_condition` restricts trust to a single Nullify AWS IAM role. Even if the WIF pool is enumerated, only the exact Nullify principal can mint a token. This matches the `external_account` credential JSON the Nullify backend synthesises in `hyperdrive/pkg/cloudintegrations/gcp/auth.go`.
Two scoping modes
Customer flow
Test plan
🤖 Generated with Claude Code