Skip to content

Add Customer Managed Encryption Key (CMEK) support to Cluster Toolkit - #6143

Merged
siddhartha-quad merged 18 commits into
GoogleCloudPlatform:developfrom
nagconsulting:cluster-toolkit-cmek-module
Oct 6, 2026
Merged

siddhartha-quad merged 18 commits into
GoogleCloudPlatform:developfrom
nagconsulting:cluster-toolkit-cmek-module

Conversation

@ep-nag

@ep-nag ep-nag commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Add Cloud KMS (CMEK) support for customer-managed encryption

Summary

Adds Cluster Toolkit support for encrypting deployed resources with a customer-managed encryption key (CMEK) instead of Google-managed keys, via three new security modules plus wiring into the modules that can already accept a CMEK key.

What's new

Three new modules (community/modules/security/):

  • kms-key — creates a symmetric Cloud KMS CryptoKey, either in a new key ring or an existing one. Defaults to deletion_policy = "DELETE", matching the underlying google_kms_crypto_key resource's own provider default: terraform destroy schedules the key's version(s) for destruction, making anything encrypted with them permanently unrecoverable. Set deletion_policy = "ABANDON" for a key whose data must outlive the deployment. Neither setting frees the CryptoKey name or key ring — Cloud KMS never deletes either at the API level.
  • pre-existing-kms-key — looks up a CryptoKey that already exists (e.g. provisioned by a security team, or in a dedicated key project) and publishes its id. Nothing is created, so terraform destroy never touches the key, regardless of kms-key's deletion_policy setting elsewhere in the same blueprint.
  • kms-key-iam — grants roles/cloudkms.cryptoKeyEncrypterDecrypter to the service agents that need to use the key, and re-exports the key id ordered behind those grants under the input name each consumer expects (so use: wires it automatically). CMEK consumers should use this module rather than kms-key/pre-existing-kms-key directly, to avoid a race between resource creation and the IAM grant.

Wiring into existing modules:

  • modules/file-system/filestore — accepts kms_key_name (ZONAL/REGIONAL/ENTERPRISE tiers only)
  • modules/packer/custom-image — accepts image_encryption_key
  • community/modules/container/artifact-registry — accepts repository_kms_key_name
  • community/modules/database/slurm-cloudsql-federation — accepts encryption_key_name
  • Slurm boot disks and the controller's config bucket already accepted disk_encryption_key/slurm_bucket_kms_key; kms-key-iam re-exports under those names too

Two new example blueprints (community/examples/):

  • kms-key.yaml — one shared generated key for an entire Slurm cluster (controller, login, nodesets, Filestore)
  • kms-key-per-service.yaml — one key per resource class (least privilege), including a Filestore key adopted via pre-existing-kms-key to demonstrate the import path

Testing

Per-module testing detail now lives in each module's own README (kms-key, pre-existing-kms-key, kms-key-iam).

Static:

  • terraform validate passes on all three new modules
  • make tests passes — full validate_configs sweep including both new example blueprints
  • pre-commit run --all-files — all substantive hooks pass (Terraform fmt/validate/tflint, pytest, mypy, go vet, golangci-lint, yamllint, shellcheck, codespell)

Live GCP verification (trimmed full-Slurm-cluster blueprints, plus minimal standalone key deployments, against a real project):

  • Generated key created with correct grants (compute, storage, filestore service agents)
  • Controller boot disk confirmed encrypted with the generated key (gcloud compute disks describe)
  • Filestore instance confirmed encrypted with the generated key (gcloud filestore instances describe)
  • Real Slurm job completed successfully on a CMEK-encrypted compute node
  • Imported key correctly adopted; terraform state list confirms it's held only as a data source, never a managed resource
  • Imported key's disk/Filestore usage confirmed encrypted with it
  • Disabling the key produced explicit lockout: DISABLED-state rejection on Compute Engine instance start (plus a kmsKeyError system event forcing TERMINATED), Filestore SUSPENDED, Cloud Storage read failed with KEY_DISABLED
  • No resources were deleted during lockout — all three remained listed, only access was blocked
  • Re-enabling the key restored access on all three without any redeployment
  • deletion_policy = "DELETE" (new default): generated key's terraform destroy schedules it for destruction (DESTROY_SCHEDULED, destroyTime set destroy_scheduled_duration out) — reproduced twice, including an independent run
  • deletion_policy = "ABANDON": terraform destroy leaves the key ring, CryptoKey and every version intact and ENABLED
  • Imported key confirmed left untouched by terraform destroy regardless of the other module's deletion_policy — reproduced independently
  • Full teardown verified clean for both example-blueprint deployments

@ep-nag
ep-nag requested a review from a team as a code owner August 13, 2026 15:14
@ep-nag
ep-nag requested review from shivam222 and shubpal07 August 13, 2026 15:14
@github-actions github-actions Bot added the external PR from external contributor label Aug 13, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request adds comprehensive support for Customer Managed Encryption Keys (CMEK) to the Cluster Toolkit. By introducing dedicated security modules for key management and IAM, users can now encrypt various infrastructure components—including disks, buckets, registries, and databases—using their own keys. This change enhances security posture and provides better control over data encryption, while ensuring proper ordering of resource creation and IAM grants to prevent race conditions.

Highlights

  • New Security Modules: Introduced three new modules: kms-key for key creation, pre-existing-kms-key for adopting existing keys, and kms-key-iam for managing service agent IAM grants.
  • Resource Integration: Updated Filestore, Packer, Artifact Registry, and Cloud SQL modules to accept CMEK configuration, enabling encrypted resource deployment.
  • Example Blueprints: Added two new example blueprints, kms-key.yaml and kms-key-per-service.yaml, to demonstrate shared and least-privilege key management patterns.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new security modules (kms-key, kms-key-iam, and pre-existing-kms-key) to support Customer-Managed Encryption Keys (CMEK) across various Cluster Toolkit components, including Slurm clusters, Filestore, Artifact Registry, Cloud SQL, and Packer custom images. The review feedback highlights potential plan-time evaluation issues in the new modules. Specifically, it recommends using try() when indexing primary[0] in outputs to prevent crashes when values are computed or empty, and suggests using conditional operators in variable validation blocks to avoid plan-time crashes caused by the lack of short-circuit evaluation in Terraform when parsing duration strings.

Comment thread community/modules/security/kms-key/outputs.tf
Comment thread community/modules/security/pre-existing-kms-key/outputs.tf
Comment thread community/modules/security/kms-key/variables.tf
Comment thread community/modules/security/kms-key/variables.tf
@aslam-quad

Copy link
Copy Markdown
Contributor

/gcbrun

@arpit974
arpit974 self-requested a review August 17, 2026 03:28
@arpit974 arpit974 self-assigned this Aug 17, 2026
Comment thread community/modules/security/kms-key/variables.tf Outdated
Comment thread community/modules/security/kms-key-iam/outputs.tf
Comment thread community/modules/security/kms-key-iam/variables.tf
@ep-nag

ep-nag commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Changes made in response to review

  • deletion_policy is now mandatory, no default (was previously defaulting to DELETE). A reviewer flagged that defaulting to DELETE risks silent data loss for anyone copying a blueprint without reading the docs; Google's guidance was to make the field required and document ABANDON as the recommended choice rather than picking either value silently. Live-verified: omitting it now fails at ghpc create time, before Terraform is even invoked, surfacing the full ABANDON/DELETE guidance directly in the error.
  • custom_service_accounts replaces service_agent_principals on kms-key-iam, per review feedback that a dedicated, purpose-built input for custom/user-managed service accounts is clearer than a generic "any principal string" variable. Live-verified standalone and combined with service_agents on the same key (both grants coexisting correctly), including the exact combination used by kms-key-per-service.yaml's disk_key_iam.
  • skip_iam_role_grants added to kms-key-iam, addressing a reviewer-identified gap: an operator adopting a key via pre-existing-kms-key whose IAM is managed out-of-band by a security team had no way to use this module's use-wiring convenience without Terraform also attempting (and failing) to create the grant itself. Live-verified end-to-end, including a negative control (revoking the manual grant and confirming boot fails with an explicit cloudkms.cryptoKeyVersions.useToDecrypt permission error, then restoring it and confirming recovery) to rule out a false positive.
  • Documentation maintenance note added to kms-key-iam's README ("Adding a new consumer"), explaining the exact obligation when giving a new Cluster Toolkit module CMEK support: a matching named output must be added here too, not just a variable on the new module.

@aslam-quad

aslam-quad commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Please NOTE : We recently upgraded our repository's Go version to 1.26 (#6173). We strongly recommend rebasing your open branches onto the latest develop branch to avoid or resolve any PR test failures.

Thank you!

arpit974
arpit974 previously approved these changes Aug 24, 2026

@arpit974 arpit974 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM,
+1 to aslam,
please rebase this PR

@arpit974 arpit974 added the release-improvements Added to release notes under the "Improvements" heading. label Aug 24, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

this PR has been inactive for 7 days and has no unresolved comments. @GoogleCloudPlatform/hpc-toolkit, please review.

@arpit974

arpit974 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

/gcbrun

@arpit974 arpit974 removed their assignment Sep 2, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

this PR has been inactive for 5 days and has no unresolved comments. @ep-nag, please review.

@github-actions

Copy link
Copy Markdown

this PR has been inactive for 2 days and has no unresolved comments. @ep-nag, please review.

@github-actions

Copy link
Copy Markdown

this PR has been inactive for 3 days and has no unresolved comments. @ep-nag, please review.

@AdarshK15
AdarshK15 removed the request for review from shivam222 September 17, 2026 14:23
Comment thread community/examples/kms-key.yaml
Comment thread community/modules/security/pre-existing-kms-key/main.tf
@vikramvs-gg

Copy link
Copy Markdown
Contributor

@ep-nag Given a couple of review comments. Also, please rebase the PR onto latest develop to get the pre-commit checks passing.

@vikramvs-gg

Copy link
Copy Markdown
Contributor

/gcbrun

vikramvs-gg
vikramvs-gg previously approved these changes Sep 22, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

this PR has been inactive for 2 days and has no unresolved comments. @arpit974, please review.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

This is a second reminder that this PR has been inactive for 4 days and has no unresolved comments. @arpit974, please review.

@arpit974 arpit974 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ep-nag The CMEK updates in b9484a0 look good, but the recent merge accidentally pulled in 30 unrelated files under community/front-end/ofe/ and community/modules/container/apptainer-* (which also reverts fixes from #6047).

Could you please rebase/clean up the branch against upstream/develop so that only the 38 CMEK files remain in the diff?

ep-nag added 17 commits October 5, 2026 11:37
Three modules for encrypting Cluster Toolkit resources with a
customer-managed key:

  kms-key                creates a CryptoKey, in a key ring it creates or
                         one that already exists
  kms-key-iam            grants the service agents that encrypt with a key
  pre-existing-kms-key   looks up a key the blueprint does not own

Creating and granting are separate because the grant is what consumers
must depend on. A CryptoKey id is available as soon as the key exists,
which is before any service agent can use it, so a resource wired to the
creating module can be created first and fail with a KMS
PERMISSION_DENIED that depends only on how Terraform scheduled the two.
kms-key-iam re-exports the id behind its grants, which also reverses the
order on destroy so an encrypted resource is removed before the grant it
relies on.

Each output is named after the input variable it feeds --
kms_key_name for Filestore, disk_encryption_key for the Slurm instance
modules, slurm_bucket_kms_key for the controller's bucket -- because
`use` matches an output name to an input name exactly. Wiring a consumer
to the creating module instead is a loud failure rather than a silent
loss of ordering: kms-key publishes no output matching any consumer's
input, so `use` matches nothing and test_module_not_used rejects the
blueprint.

Service-agent addresses are derived from the project number via a
google_project data source, following the pattern in bigquery-sub, so a
blueprint says `service_agents: [compute, storage]` rather than carrying
service-PROJECT_NUMBER@... strings. Explicit principals remain available
for agents in another project, which cannot be derived.

The CryptoKey defaults to deletion_policy = "ABANDON", so teardown never
destroys key material: data encrypted with a key routinely outlives the
deployment that created it, and destroying versions cannot be undone.
pre-existing-kms-key is stronger still -- the key is never in Terraform
state, so destroy cannot touch it.
Adds a customer-managed encryption key input to the modules a CMEK
cluster needs but which had no way to take one:

  modules/file-system/filestore              kms_key_name
  modules/packer/custom-image                image_encryption_key
  community/modules/container/artifact-registry
  community/modules/database/slurm-cloudsql-federation

Filestore gains a precondition restricting CMEK to the ZONAL, REGIONAL
and ENTERPRISE tiers. The BASIC tiers do not support it at all, so a key
set on one is a mistake worth reporting at plan time rather than
silently dropping.

Cloud SQL and Artifact Registry also encrypt the Secret Manager secret
holding their credentials, so both take a key for the secret as well as
for the instance or repository.

The packer golden-copy expectations move with the custom-image change.
Two blueprints, both complete Slurm clusters.

kms-key.yaml is the single-key case: one CryptoKey encrypting the
Filestore /home, the controller, login and compute boot disks, an
additional scratch disk, and the Slurm configuration bucket. Everything
is wired with `use`, so no key name is written out by hand.

kms-key-per-service.yaml is the same cluster with one key per class of
resource, each granted only to the agent that uses it, so compromising
one agent does not expose data belonging to another. It also shows
adopting a key created outside the blueprint, and overriding the
encryption identity with disk_encryption_key_service_account.

The additional_disks entries name their key explicitly. `use` matches a
module output to an input variable, and additional_disks is a
caller-supplied list of objects with no input to bind to, so the boot
disk is covered automatically and an additional disk would otherwise
fall back to Google-managed encryption without saying so.

Both were applied and destroyed on real infrastructure. The per-service
cluster confirmed the three keys were granted to disjoint sets of
agents, that a dynamically created compute node carried both its boot
and scratch disks on the disk key, and that the adopted key was left
untouched by destroy.
…liases

The kms-key, kms-key-iam and pre-existing-kms-key modules all declare
cloudkms.googleapis.com, but neither example enabled it, so a project
that had never used Cloud KMS failed test_apis_enabled before apply.
Add a service-enablement module to both, with the validator skip that
makes it reachable -- test_apis_enabled runs at expansion time, before
any module applies, so without the skip the module never gets to run.

Add repository_kms_key_name and encryption_key_name outputs to
kms-key-iam so `use` wires artifact-registry and slurm-cloudsql-federation
the way it already wires Filestore, disks, the Slurm bucket and images.
Both carry the same depends_on as the other outputs, so consumers are
ordered behind the grants rather than racing them.

Outputs are deliberately not added for boot_disk_kms_key (GKE),
kms_key (managed-lustre) or disk_encryption_kms_key (gke-storage):
kms-key-iam has no service agent for those, so wiring a key without a
grantable agent would produce exactly the silent PERMISSION_DENIED the
two-module split exists to prevent.
Paginate the Cloud KMS listing with list_next: a project with more than
one page of key rings, or a ring with more than one page of keys, silently
lost the remainder, and a missing suggestion is indistinguishable from a
key that does not exist.

Accept HIGH_SCALE_SSD wherever CMEK is allowed. It is Google's legacy name
for ZONAL and the same tier underneath, so matching on the literal string
blocked callers using the older name -- including OFE, which offers
HIGH_SCALE_SSD rather than ZONAL -- from any CMEK Filestore below the far
more expensive ENTERPRISE tier. Fixed in the module precondition, not just
in OFE, since the module is where the rule lives.

Validate rotation_period and destroy_scheduled_duration at plan time. Both
are duration strings the API constrains, and destroy_scheduled_duration is
immutable, so a bad value applied by mistake cannot be corrected in place.

Detect the test environment with settings.TESTING rather than sys.argv, so
pytest and coverage runners are covered too; the argv check stays as a
fallback for `manage.py test` run without --settings=website.test_settings,
which would otherwise fail on a missing configuration.yaml.

Guard normalize_location against a missing location, cast the credentials
Path to str before putting it in a subprocess environment, and drop a
redundant json round-trip over a value that was already a dict. The last
two are pre-existing issues in files this change already touches.

Drop the migrations rule from the committed .gitignore: generating Django
migrations at deploy time is a real problem, but it predates this change
and fixing it means altering OFE's deployment model. The local exclude
keeps them out of a working tree without asserting the practice here.

Make every credentials env value a str, not just the one in _run_ghpc:
_run_ghpc_import_inputs and the three extra_env dicts passed to
run_terraform/run_packer all reached subprocess env as Path objects.
Python coerces os.PathLike env values, so this was inconsistency rather
than a defect, but the file already used .as_posix() elsewhere.

Fix the Filestore CMEK remediation text, which still told users to move to
ENTERPRISE after HIGH_SCALE_SSD became acceptable -- advice to pay for a
more expensive tier than they needed. Add a test that parses the message
and asserts every tier it names is actually allowed
The per-service example and the pre-existing-kms-key README named a key
ring and key that exist in a real test project, so a reader who forgot to
substitute them could resolve against live resources and encrypt with
someone else's key instead of failing at plan time. Use the my-* form the
neighbouring hpc-slurm-kms.yaml already uses; names that cannot
accidentally exist.
…MBER

rather than a numeric project number, and my-keyring/my-key rather than
four spellings of the same thing, matching hpc-slurm-kms.yaml.
A malformed key_ring_id aborted the plan instead of failing validation.
"my-keyring" or "projects/p" has fewer than four slash-separated segments,
so indexing [1] or [3] raised an evaluation error before Terraform printed
any validation message -- the caller never saw the format error that would
have told them what was wrong. Index through try() so a short id yields ""
and falls through to a clean message, with the format error reported first.
This also removes the coalesce() fallbacks, which existed only to keep
those indexes in range.

A CryptoKey whose only version was destroyed comes back with
"primary": null, and .get("primary", {}) returns None for an explicit null
because a default only applies to a missing key -- so reading .state off it
raised AttributeError and the whole listing failed. Use `or {}`.

A non-numeric credential id reached the ORM, where the field conversion
raises ValueError rather than the DoesNotExist that get_object_or_404
catches, surfacing as an unhandled 500. Reject it with a 400 before the
lookup. A 400 rather than an empty list: a 200 saying "no keys" would be
untrue when the parameter was never a valid id, and the datalist treats any
non-2xx the same way.

Adds KmsKeyListingRobustnessTests covering all three, including that a
numeric id still reaches the lookup and queries both the region and
"global".
terraform-readme and packer-readme had drifted from their modules'
variable definitions: artifact-registry, slurm-cloudsql-federation,
filestore and custom-image all gained a CMEK input in an earlier commit
without a doc regeneration. Also fixes an escaping inconsistency in
kms-key-iam's generated table and two missing trailing newlines.
…ardown

kms-key's deletion_policy defaulted to ABANDON, so terraform destroy always
left a generated key's CryptoKeyVersion enabled in Cloud KMS regardless of
destroy_scheduled_duration -- that setting existed on the resource but never
took effect, since ABANDON removes the CryptoKey from Terraform state
without calling the API at all. A key this module created is this
deployment's to own, so its teardown should destroy the key material with
it by default, not strand it enabled forever.

Flip the default to DELETE, matching the google_kms_crypto_key resource's
own provider default. terraform destroy now schedules the CryptoKeyVersion
for destruction (confirmed live: ENABLED -> DESTROY_SCHEDULED, destroyTime
set destroy_scheduled_duration out). Set deletion_policy = ABANDON on a key
whose data must outlive its deployment.

pre-existing-kms-key needs no change and is unaffected either way: it only
reads a key via data sources and never creates a google_kms_crypto_key
resource, so it has nothing for `terraform destroy` to act on regardless of
this module's deletion_policy (confirmed live: terraform destroy reports
nothing to destroy, key stays ENABLED).

Neither setting frees the CryptoKey name or key ring -- Cloud KMS never
deletes either at the API level -- so redeploying under the same names
still requires a fresh key_name/key_ring_name or deployment_name either way.

Updates kms-key's and pre-existing-kms-key's READMEs and the two CMEK
example blueprints' teardown notes to match the new default.
…ADMEs

Add a Testing section to kms-key, pre-existing-kms-key and kms-key-iam
covering what was actually verified for each: static terraform validate,
and live-GCP results specific to that module (grant placement, consumer
wiring, lockout/recovery behavior, and the kms-key deletion_policy default
of DELETE vs pre-existing-kms-key's unconditional no-touch guarantee).
Keeps the evidence next to the code it documents rather than only in the
PR description.
…ADMEs

Add a Testing section to kms-key, pre-existing-kms-key and kms-key-iam,
each showing the actual gcloud/ghpc commands and output used to confirm
the module does what it claims, rather than prose describing testing that
happened elsewhere:

- kms-key: gcloud kms keys versions list before/after `terraform destroy`
  under both deletion_policy values, showing DESTROY_SCHEDULED vs ENABLED
- pre-existing-kms-key: deploy/destroy against an out-of-band key showing
  both report nothing to do, terraform state list showing only data
  sources, and the key still ENABLED afterward regardless of any kms-key
  module's deletion_policy elsewhere in the blueprint
- kms-key-iam: gcloud kms keys get-iam-policy showing the compute/storage/
  filestore service-agent grants actually landed

Each section covers only what that module is responsible for and links to
the others rather than repeating the same evidence three times. All
command/resource names are placeholders (PROJECT_ID, KEYRING, KEY, etc.),
matching the placeholder convention already used elsewhere in these
READMEs.
…vice_accounts

Upstream review (PR GoogleCloudPlatform#180, comment on kms-key-iam/variables.tf) pointed out
that a custom/user-managed service account is a distinct, common case
service_agents can't derive, and asked for a dedicated input rather than
overloading a generic "any principal string" variable for it. In practice
service_agent_principals was only ever used for custom SAs, so replace it
outright rather than add a second variable alongside it.

custom_service_accounts takes bare SA emails (no "serviceAccount:" prefix --
the module adds it) instead of fully-qualified principal strings, with
validation rejecting a mistakenly-prefixed value and requiring a
".gserviceaccount.com" address. Unioned with service_agents exactly as
service_agent_principals was; the underlying google_kms_crypto_key_iam_member
for_each loop is unchanged.

Updates the one caller (community/examples/kms-key-per-service.yaml, now
using service_account_email instead of service_account_iam_email) and the
example blocks in all three module READMEs that had been using
service_agent_principals to spell out a derived agent by hand instead of
using service_agents, which existed for exactly that case.

Live-verified against a real GCP project, replacing the two scenarios this
was meant to address:
- a single custom SA granted with no service_agents at all, confirmed via
  IAM policy and disk kmsKeyServiceAccount on the controller, login and a
  dynamically-provisioned compute node, plus a real Slurm job on the latter
- two custom SAs granted on one key and pointed at two different resources
  (controller+login on one, the nodeset on the other), confirming each
  disk used the SA it was actually assigned rather than either being a
  catch-all

Both scenarios reproduced independently. terraform validate, make tests and
pre-commit run --all-files all pass with no new failures beyond this
sandbox's pre-existing environment-tooling gaps (terraform-docs, addlicense,
goimports, gocyclo not installed).

Documents both scenarios with real gcloud command/output evidence in
kms-key-iam's README, matching the existing per-module Testing section
convention.
Upstream review (PR GoogleCloudPlatform#180) flagged that defaulting deletion_policy to DELETE
is dangerous for anyone who copies a blueprint without reading the variable
docs: running terraform destroy on what they thought was just tearing down
compute would also cryptographically shred their key's data, silently.

Google's guidance on the thread: make the field mandatory so Terraform
fails without it, and document ABANDON as the recommended choice for most
blueprints -- rather than picking either value silently on the caller's
behalf.

Drop the "DELETE" default from kms-key's deletion_policy variable (already
nullable = false, so omitting it now fails validation instead of falling
back). Rewrite the variable description, the main.tf comment, and the
"Lifecycle and naming" section of kms-key's README to lead with "required,
no default" and explain why ABANDON is recommended over DELETE rather than
asserting either as the default. Fix pre-existing-kms-key's README
cross-reference, which described the old default.

Update both example blueprints (kms-key.yaml, kms-key-per-service.yaml),
which relied on the removed default, to set deletion_policy: ABANDON
explicitly on every kms-key module instance, with a comment on why; their
teardown-note comments are rewritten to match.

Live-verified: omitting deletion_policy now fails at `ghpc create` time,
before Terraform is invoked at all, surfacing the full ABANDON/DELETE
guidance directly in the error rather than sending the caller to the
README. Both example blueprints re-confirmed to expand cleanly with
deletion_policy set. DELETE's and ABANDON's actual teardown behavior are
unchanged from prior testing (DESTROY_SCHEDULED vs. ENABLED respectively)
-- this change only affects whether a value must be chosen, not what either
value does.

make tests and pre-commit run --all-files both pass, no new failures beyond
this sandbox's pre-existing environment-tooling gaps (terraform-docs,
addlicense, goimports, gocyclo not installed).

Documents the required-field behavior with real command/output evidence in
kms-key's README Testing section, matching the existing convention.
…missions

Upstream review (PR GoogleCloudPlatform#180, comment 3b) identified a real gap: an operator
adopting a key via pre-existing-kms-key whose IAM is managed out-of-band by
a separate security team has no way to get kms-key-iam's output-aliasing
convenience (use: auto-wiring to kms_key_name, disk_encryption_key, etc.)
without Terraform also attempting to create the
google_kms_crypto_key_iam_member grants itself -- which fails with a 403
if the identity running Terraform lacks cloudkms.admin/setIamPolicy on
that key, exactly the point of "managed out-of-band."

Add skip_iam_role_grants (bool, default false, matching the reviewer's
suggested name): when true, gates google_kms_crypto_key_iam_member.this's
for_each to an empty set, following this repo's existing
`cond ? real : []` idiom for conditionally-created for_each resources
(see schedmd-slurm-gcp-v6-controller/controller.tf). Every output still
resolves crypto_key_id normally -- depends_on on a for_each resource is a
graph-level dependency and resolves cleanly even at zero instances.

Cross-variable validation rejects skip_iam_role_grants = true combined
with a non-empty service_agents or custom_service_accounts, since either
would otherwise look like a grant request that silently does nothing.

Live-verified against a real GCP project:
- skip_iam_role_grants = true with a non-empty service_agents fails
  terraform plan immediately with the new validation error -- confirmed
  zero resources are created, not even the key ring, since plan-level
  variable validation aborts before any resource is planned
- a key created and granted entirely by hand (gcloud kms keys
  add-iam-policy-binding, simulating a security team managing permissions
  out-of-band), then deployed via pre-existing-kms-key -> kms-key-iam
  (skip_iam_role_grants = true) -> a Slurm controller and login node:
  terraform state list shows zero google_kms_crypto_key_iam_member
  resources, both disks correctly resolved and used the manually-granted
  key, and the login instance booted and was reachable

terraform validate, make tests and pre-commit run --all-files all pass
with no new failures beyond this sandbox's pre-existing environment-
tooling gaps (terraform-docs, addlicense, goimports, gocyclo not
installed) and one confirmed-unrelated, non-reproducing flaky module-fetch
failure in make tests (transient HTTP 500 from an upstream git host,
hitting a different unrelated example config on each retry).

Documents the new variable with a "Out-of-band permissions" section and
real command/output evidence in kms-key-iam's README, and cross-references
it from pre-existing-kms-key's Requirements section.
…missions

Upstream review (PR GoogleCloudPlatform#180, comment 3b) identified a real gap: an operator
adopting a key via pre-existing-kms-key whose IAM is managed out-of-band by
a separate security team has no way to get kms-key-iam's output-aliasing
convenience (use: auto-wiring to kms_key_name, disk_encryption_key, etc.)
without Terraform also attempting to create the
google_kms_crypto_key_iam_member grants itself -- which fails with a 403
if the identity running Terraform lacks cloudkms.admin/setIamPolicy on
that key, exactly the point of "managed out-of-band."

Add skip_iam_role_grants (bool, default false, matching the reviewer's
suggested name): when true, gates google_kms_crypto_key_iam_member.this's
for_each to an empty set, following this repo's existing
`cond ? real : []` idiom for conditionally-created for_each resources
(see schedmd-slurm-gcp-v6-controller/controller.tf). Every output still
resolves crypto_key_id normally -- depends_on on a for_each resource is a
graph-level dependency and resolves cleanly even at zero instances.

Cross-variable validation rejects skip_iam_role_grants = true combined
with a non-empty service_agents or custom_service_accounts, since either
would otherwise look like a grant request that silently does nothing.

Live-verified against a real GCP project, including a negative control to
rule out a false positive:
- skip_iam_role_grants = true with a non-empty service_agents fails
  terraform plan immediately with the new validation error -- confirmed
  zero resources are created, not even the key ring
- a key created and granted entirely by hand (gcloud kms keys
  add-iam-policy-binding, simulating a security team managing permissions
  out-of-band), then deployed via pre-existing-kms-key -> kms-key-iam
  (skip_iam_role_grants = true) -> a Slurm controller and login node:
  terraform state list shows zero google_kms_crypto_key_iam_member
  resources, both disks correctly resolved and used the manually-granted
  key, and the login instance booted and was reachable
- negative control: removed the manual grant on that same key and forced a
  fresh decrypt via stop/start on the same controller instance -- the
  start API call itself was explicitly rejected
  (Permission 'cloudkms.cryptoKeyVersions.useToDecrypt' denied, citing the
  exact key), and the instance went to TERMINATED. Re-adding the identical
  binding immediately restored it. Same key, same instance, only the grant
  changed, confirming the earlier success was actually caused by that
  grant rather than some unrelated permission already present in the
  project. Reproduced independently by the user as well.

terraform validate, make tests and pre-commit run --all-files all pass
with no new failures beyond this sandbox's pre-existing environment-
tooling gaps (terraform-docs, addlicense, goimports, gocyclo not
installed) and one confirmed-unrelated, non-reproducing flaky module-fetch
failure in make tests (transient HTTP 500 from an upstream git host,
hitting a different unrelated example config on each retry).

Documents the new variable with a "Out-of-band permissions" section and
real command/output evidence -- including the negative control -- in
kms-key-iam's README, and cross-references it from pre-existing-kms-key's
Requirements section.
The entry still described the single-module design: it credited kms-key
with making the grants and told authors to write `use: [kms_key]`, which
does not merely fail to wire anything -- none of kms-key's output names
(crypto_key_id, key_ring_id, primary_crypto_key_version_id) match a
consumer's CMEK input, so gcluster rejects the blueprint with
test_module_not_used. Only kms-key-iam's outputs match. The blueprint
itself was already correct; only its index entry was left behind.

Also drops filestore_service_agent/compute_service_agent/
storage_service_agent, settings that do not exist anywhere in this
codebase -- the blueprint provisions service agents via gcloud comments
and kms-key-iam derives each address from project_id, no manual address
ever appears.

Corrects the teardown paragraph, which claimed destroy retains key
material unconditionally. deletion_policy is now required with no
default; this blueprint chooses ABANDON, and DELETE does the opposite.
@ep-nag
ep-nag force-pushed the cluster-toolkit-cmek-module branch from dac1430 to 5795aaf Compare October 5, 2026 10:42
@arpit974

arpit974 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

@ep-nag Thanks for cleaning up the branch!
It looks like commit b9484a0 (your fixes for @vikramvs-gg's review comments—adding provider_meta "google" to the 3 versions.tf files and moving enable_apis into a setup group in the 2 example YAML files) got dropped during the rebase. Could you please re-apply b9484a0 on top of this branch.

Add provider_meta "google" to the kms-key, pre-existing-kms-key and
kms-key-iam versions.tf files so the modules emit GCP module usage
telemetry and are stamped by tools/create-release-candidate.sh.

Move enable_apis into its own deployment group in the kms-key and
kms-key-per-service blueprints. service-enablement publishes no outputs,
so within a single group nothing orders key ring creation after
google_project_service; a preceding group makes it an ordering rather
than a race.

@arpit974 arpit974 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@arpit974

arpit974 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

/gcbrun

@siddhartha-quad
siddhartha-quad merged commit a0a64d4 into GoogleCloudPlatform:develop Oct 6, 2026
25 of 85 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from external contributor release-improvements Added to release notes under the "Improvements" heading.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants