Skip to content

Add Service Account Profiles Module - #6433

Open
ep-nag wants to merge 10 commits into
GoogleCloudPlatform:developfrom
nagconsulting:service-account-profiles
Open

ep-nag wants to merge 10 commits into
GoogleCloudPlatform:developfrom
nagconsulting:service-account-profiles

Conversation

@ep-nag

@ep-nag ep-nag commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds community/modules/iam/service-account-profiles, which grants a named, least-privilege
permission profile to an existing service account. It replaces the broad predefined roles the
Slurm example blueprints grant today, such as the controller's project-wide
roles/compute.instanceAdmin.v1 and roles/iam.serviceAccountUser (the latter lets it impersonate
every service account in the project).

Account creation stays with the existing modules/project/service-account module; this module only
grants permissions. Each identity is therefore two blocks: modules/project/service-account (with
project_roles: []) creates the keyless account, and a service-account-profiles block with
use: [<account module id>] grants the profile's permissions to it.

  • Profiles:

    • slurm-controller gets a custom role holding only the Compute Engine permissions the Slurm
      controller's resume, suspend and sync scripts call.
    • image-builder gets a custom role for the Packer build VM.
    • custom gets a custom role built from the permissions and existing roles you list.
    • slurm-login and slurm-compute get no custom role: only the baseline below, plus any
      resource-scoped bindings you add.

    Every profile also gets the logging.logWriter / monitoring.metricWriter baseline.

  • Name-scoped compute.instances.setMetadata: the controller and image builder need this
    permission, but it is never granted project-wide. It is held in a second custom role, bound with an
    IAM condition that matches only instances whose names start with instance_name_prefixes. The
    defaults are the cluster's <slurm_cluster_name>- and Packer's packer-.

  • Opt-in capability flags, accepted by slurm-controller, each adding only the permissions for
    one feature:

    • enable_placement_policies: compact placement policies;
    • enable_public_ips: nodes with external IPs;
    • enable_bigquery_load: BigQuery job accounting;
    • enable_tpu: Cloud TPU VM nodesets (schedmd-slurm-gcp-v6-nodeset-tpu);
    • enable_mig: nodesets in a Terraform-created MIG (provisioning_engine = "MIG");
    • enable_mig_flex: DWS Flex nodesets;
    • enable_tpu_gce: TPU machine types on Compute Engine (ct*/tpu* in a regular nodeset).

    The three MIG-based flags (enable_mig, enable_mig_flex and enable_tpu_gce) grant
    setMetadata project-wide, because IAM Conditions cannot scope an instance group manager. The
    README warns about this, and the custom profile refuses those three; it accepts the rest.

  • roles/iam.serviceAccountUser on individual accounts only: the controller gets it on each
    compute account it attaches to nodes, and the image builder on itself. Never project-wide, and only
    on accounts in the same project.

  • Resource-scoped bindings: grant a role on one bucket, secret, Artifact Registry repository,
    BigQuery dataset, Pub/Sub topic or subscription, or subnetwork (for Shared VPC), instead of
    project-wide.

  • Plan-time input checks:

    • custom_permissions refuses permissions that allow escalation: impersonation, key management,
      any *.setIamPolicy except pubsub.subscriptions.setIamPolicy, secret and KMS access, and VM
      login or metadata.
    • Bindings refuse basic roles and roles/iam.* / roles/resourcemanager.* roles.
  • Lifecycle inputs: custom_role_stage and custom_role_deletion_policy for the generated
    roles, and iam_propagation_wait, a pause before the service_account_email output is available
    so the grants have time to take effect.

Files touched

  • The module (new): community/modules/iam/service-account-profiles/: main.tf, variables.tf,
    outputs.tf, versions.tf, metadata.yaml and README.md.
  • Blueprints converted to the module, or added:
    • community/examples/hpc-slurm-service-account-profiles.yaml (new): the reference example, with
      two compute nodesets, each with its own compute account.
    • examples/hpc-enterprise-slurm.yaml: converted from broad predefined roles.
    • community/examples/hpc-slurm-ha.yaml: converted. Both HA controllers share one identity. Its
      Filestore size_gb was raised to the BASIC_SSD minimum (2560); the blueprint could not deploy
      without it.
    • community/examples/hpc-slurm6-tpu.yaml: converted, with enable_tpu, a dedicated account for
      the TPU nodes, and the controller's actAs on that account.
    • examples/image-builder.yaml: converted, with the image-builder profile on the Packer build VM
      and separate controller, login and compute identities for the cluster.
  • Fix outside the module: .../slurm_files/scripts/resume.py and tests/test_resume.py (see
    "Fix outside the module" below).
  • Registration and tests:
    • modules/README.md and examples/README.md list the module and the new example.
    • tools/validate_configs/test_configs/service-account-profiles.yaml exercises every profile and
      capability flag; its README describes it.
    • A new daily test for the reference example:
      tools/cloud-build/daily-tests/{builds,tests}/hpc-slurm-service-account-profiles.*, the
      IAM-assertion playbook ansible_playbooks/test-validation/test-service-account-profiles.yml, and
      its Kueue test lock.
    • The hpc-enterprise-slurm, slurm-gcp-v6-tpu and packer daily-test build files gain the
      module's tags, so those tests run when the module changes. The TPU daily test also runs the
      IAM-assertion playbook.

How the permission set was derived

By deploying real clusters, reading resume.log / slurmsync.log / audit logs, and tightening the
role until nothing extra remained; each permission traces to an observed API call or to Google's
per-method documentation. Notable finds:

  • bulkInsert (how the controller creates nodes) needs compute.instances.setMetadata on each new
    instance, even though the controller sets no metadata, and whether or not the instance template
    carries metadata
    . That is why the controller has it, name-scoped. The same is true of setLabels,
    setServiceAccount, setTags and disks.setLabels.
  • compute.zoneOperations.list, globalOperations.list and regionOperations.list are needed by
    resume.py's failure diagnosis and repair.py's operation polling. slurmsync swallows the
    exception, so missing them fails silently.
  • compute.resourcePolicies.list / .delete are in the base controller role, because slurmsync
    cleans up placement policies on every cycle, whether or not placement is used.
  • compute.images.useReadOnly is needed to create nodes from a custom image in the project.
  • compute.instanceGroups.delete is needed by enable_mig_flex and enable_tpu_gce: deleting a MIG
    also deletes its instance group, which is checked separately. compute.instanceGroups.create is
    needed by enable_tpu_gce for the zonal MIG it creates per TPU slice.
  • bigquery.jobs.create and tpu.nodes.update are never called, so they are not granted.

What's been tested

  • Local:
    • make tests passes, including validate_configs for all six blueprints that use the module.
    • pre-commit passes on every changed file.
    • A local plan-time suite of about 140 cases (not part of this PR) checks every validation,
      denylist entry and rejected input combination.
  • Live, on real deployments, each torn down afterwards:
    • Reference cluster: the new daily-test playbook passes against it, checking the exact
      permission allowlist, baseline roles, role stage, actAs scope and scoped condition. Jobs on each
      nodeset run under that nodeset's own compute account. A 2-node job runs with a placement policy
      the controller creates.
    • Name-scoped setMetadata:
      • As a controller-profile account, setMetadata on an instance whose name matches the prefix
        succeeds, and on one that doesn't is denied (403).
      • bulkInsert of a non-matching name is denied, whether or not the template carries metadata.
      • Changing a live cluster's condition to a non-matching prefix stops node creation, and
        restoring it restores creation.
    • Image builder: a Packer build runs as the image-builder account, and a cluster node boots
      from the resulting image.
    • enable_mig: a static MIG nodeset starts and runs jobs. Without the flag, the controller's
      calls to list the MIG and delete its instances fail with a 403.
    • enable_mig_flex: a DWS Flex job runs in a per-job MIG that the controller creates and
      deletes.
    • enable_tpu: the controller's TPU node create is authorised and reaches provisioning, then
      stops on a capacity error (429), not a permission error.
    • enable_tpu_gce: the controller creates the TPU slice's workload policy, instance template
      and MIG, and requests the resize, with no permission errors. The node then stops on TPU capacity
      (ZONE_RESOURCE_POOL_EXHAUSTED).
    • HA: both controllers run as one account. With the primary's slurmctld stopped, the backup
      creates new nodes and runs jobs, with no extra permissions.
    • Test config: all 12 identities in service-account-profiles.yaml read back exactly as
      configured, including every generated role's permissions and all seven binding types.
    • Unsupported features: repair, performMaintenance and controller_network_attachment have no
      capability flag. Each fails with a clear 403, as the README documents.

Fix outside the module

resume.py's placement-policy error handler unpacked each failed result as a (request, error)
tuple, but the value is the bare HttpError. A denied placement-policy create (for example, a
controller without compute.resourcePolicies.create) therefore crashed resume with a TypeError,
and the missing permission was never logged. This is a one-line fix with a regression test. Now
resume.log names the denied permission, which is how a least-privilege controller's missing
permissions are diagnosed.

Gaps, maintainer decisions and future work

Gaps

  • TPU testing is blocked on availability.
    • enable_tpu is verified up to node creation; booting a node is blocked by TPU capacity (429).
    • enable_tpu_gce is verified up to MIG creation and resize; booting a node is blocked by TPU
      capacity. On-demand (dynamic) TPU-on-GCE jobs also need a slurm-gcp image with Slurm 26.05 or
      later (current images ship 25.11).

Maintainer decisions

  • CI gating: like every integration test in this repo, the new test runs nightly on develop;
    on pull requests it runs only if a maintainer approves it (auto_approved_pr_tests = []). So a
    later PR that makes the controller call a new API would be caught by the nightly run after merge,
    not before. Adding this test to auto_approved_pr_tests would catch it before merge, at the cost
    of a cluster deployment per PR.
  • Badge: hpc-enterprise-slurm.yaml is a core-badged example that now depends on this
    community/experimental module. Maintainers may want to keep the badge, re-badge the example, or
    promote the module.

Deferred work

  • End-to-end TPU validation, once TPU capacity and a slurm-gcp image with Slurm 26.05 or later
    are available.
  • Capabilities for the Slurm repair flow, opportunistic maintenance and
    controller_network_attachment.
    The permissions each needs are identified from source but have
    not been proven against a working node, so no flag was added.
  • Converting Open Front End to the module. OFE still creates its own identities with broad
    project-wide roles. The custom profile with resource-scoped bindings already covers its web
    identity (see the README example), but moving OFE itself onto the module is separate work.

@ep-nag
ep-nag requested a review from a team as a code owner October 7, 2026 10:57
@github-actions github-actions Bot added the external PR from external contributor label Oct 7, 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 introduces a new IAM module designed to enforce the principle of least privilege for Slurm clusters on GCP. By replacing broad, project-wide predefined roles with granular, profile-based custom roles and resource-scoped bindings, it significantly reduces the attack surface of service accounts. The changes include support for various cluster features via opt-in capability flags and ensure that sensitive permissions like metadata modification are strictly scoped to relevant resources.

Highlights

  • New IAM Module: Introduced community/modules/iam/service-account-profiles to provide named, least-privilege permission profiles for service accounts, replacing broad project-wide roles.
  • Enhanced Security: Implemented name-scoped compute.instances.setMetadata permissions using IAM conditions and restricted roles/iam.serviceAccountUser to individual accounts rather than project-wide.
  • Capability Flags: Added opt-in capability flags (e.g., enable_placement_policies, enable_tpu, enable_mig) to granularly grant specific permissions only when needed.
  • Blueprint Migration: Updated multiple example blueprints (hpc-enterprise-slurm, hpc-slurm-ha, hpc-slurm6-tpu, image-builder) to use the new module for improved security posture.
  • Fixes and Testing: Fixed a crash in resume.py related to placement policy errors and added comprehensive daily integration tests to validate IAM assertions.
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 a new community module, service-account-profiles, which applies named least-privilege permission profiles to existing service accounts. It updates several Slurm and image-builder examples to utilize this module instead of broad predefined roles, adds a dedicated example blueprint, updates documentation indexes, and introduces comprehensive validation tests. Feedback on the changes to ghpcfe_c2daemon.py highlights a critical bug where a return statement is incorrectly indented inside a loop, causing premature termination, and notes that the changes accidentally revert robust timeout and retry logic; it is recommended to revert this file entirely.

@aslam-quad

Copy link
Copy Markdown
Contributor

/gcbrun

@ep-nag ep-nag changed the title Squashed commit of the following: Add Service Account Profiles Module Oct 7, 2026
@ep-nag
ep-nag force-pushed the service-account-profiles branch from 6044b35 to 25516a9 Compare October 7, 2026 11:40
@aslam-quad

Copy link
Copy Markdown
Contributor

/gcbrun

@SwarnaBharathiMantena
SwarnaBharathiMantena removed their request for review October 8, 2026 08:19
@SwarnaBharathiMantena SwarnaBharathiMantena removed their assignment Oct 8, 2026
@AdarshK15
AdarshK15 self-requested a review October 8, 2026 08:21
@AdarshK15 AdarshK15 assigned AdarshK15 and unassigned ishitachail Oct 8, 2026
@AdarshK15
AdarshK15 removed the request for review from ishitachail October 8, 2026 08:22
Comment thread examples/hpc-enterprise-slurm.yaml Resolved
Comment thread community/examples/hpc-slurm-ha.yaml Resolved
@AdarshK15 AdarshK15 added the release-new-modules Added to release notes under the "New Modules" heading. label Oct 8, 2026
@github-actions

Copy link
Copy Markdown

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

@AdarshK15 AdarshK15 assigned ep-nag and unassigned AdarshK15 Oct 11, 2026
Name the modules/project/service-account block <x>_sa and the
service-account-profiles block <x>_profile, with use: [<x>_sa]. Consumers
reference <x>_profile so they still wait for iam_propagation_wait.

hpc-enterprise-slurm and hpc-slurm-ha had controller_sa, login_sa and
compute_sa on develop. Renaming them moved the accounts to new Terraform
addresses, so a redeploy planned to destroy and recreate each account.
Both blueprints now keep the develop ids. hpc-slurm-ha also restores its
develop account names (controller-sa, login-sa, compute-sa), description
and display_name, so the account ids are unchanged too. The other
blueprints, the test config, the README examples and two daily-test
comments follow the same convention.
gcluster passes a deployment variable to every module with an input of
the same name, so a blueprint that sets enable_public_ips in vars made
the slurm-login and slurm-compute profiles fail validation. Profiles
that accept no capability flags (slurm-login, slurm-compute,
image-builder) now ignore them, and the generated role is built only
from flags the profile accepts. custom still rejects enable_mig,
enable_mig_flex and enable_tpu_gce, whose setMetadata cannot be
name-scoped.

enable_placement_policies now defaults to null, meaning the profile
default: on for slurm-controller, because nodesets default to
enable_placement = true and partitions to exclusive = true, and off
for custom. The TPU and HA blueprints opt out explicitly because they
use no placement, which keeps the TPU daily test's allowlist unchanged.
A deleted custom role can be undeleted only within 7 days, and its id
cannot be reused for up to 44 days, so redeploying a destroyed
deployment under the same deployment_name fails in between. The
optional role_id_suffix input makes the generated role ids
<deployment_name>.<name>.<role_id_suffix>[.scoped]. It must be 1-8
lowercase letters or digits and not "scoped", and the name length
check counts it. The README's "Custom role lifecycle" section and each
converted blueprint explain when to set it.

metadata.yaml gains allowed_enum validators for custom_role_stage and
custom_role_deletion_policy, and regex validators for the
deployment_name and name format and their combined length, so these
fail at gcluster create instead of terraform plan.
Integration tests that use community/modules/iam/service-account-profiles
create custom roles per run. A killed run or failed destroy leaves them
behind, counting toward the project's custom-role limit.
process_custom_roles deletes only roles whose id has the module's
<deployment_name>.<name>[.<role_id_suffix>][.scoped] shape and whose
description starts with one the module sets. It skips roles matching
the exclusion list, including active Cloud Build prefixes, with dashes
compared as underscores. It runs in Phase 5 after service accounts.
No profile grants project-wide Cloud Storage access, so any account whose
VMs run a modules/scripts/startup-script script needs that module's
bucket_viewers on its staging bucket. Slurm nodes and the controller need
this too, not only the Packer build VM. A new "Storage access" section
says so, and says to use bucket_viewers, not bucket_bindings, for that
bucket, because startup-script grants roles/storage.objectViewer
authoritatively. Also explain why enable_tpu_gce needs
compute.images.get and compute.subnetworks.get.
Redeploying an existing hpc-slurm-ha or hpc-enterprise-slurm
deployment moved each account's logging.logWriter and
monitoring.metricWriter grant from modules/project/service-account to
the profile. Terraform created the profile's grant and then destroyed
the account module's grant of the same binding in the same apply. Both
are non-authoritative, so the later delete removed the binding and
logging and metrics stopped silently.

Add grant_baseline_roles (default true). The two converted core
blueprints keep the baseline roles in the account blocks' project_roles
and set grant_baseline_roles: false, so each binding keeps one owner.
Document this in a new "Converting an existing deployment" README
section, and quote the log line a missing bucket_viewers grant
actually produces.
@github-actions

Copy link
Copy Markdown

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

@github-actions

Copy link
Copy Markdown

This is a second reminder that this PR has been inactive for 7 days and has no unresolved comments. @ep-nag, please review.

Resolve conflict in community/examples/hpc-slurm-ha.yaml: keep develop's vars and the service-account-profiles identities, and default homefs_size_gb and controller_fs_size_gb to 2560 (BASIC_SSD minimum).
@aslam-quad

Copy link
Copy Markdown
Contributor

Hi @ep-nag

Could you please take a look at the pre-commit failures?

Comment thread examples/README.md Resolved
Comment thread tools/cleanup.sh Resolved
@AdarshK15

Copy link
Copy Markdown
Member

@ep-nag, Could you please rebase and fix the pre-commit failures?

@AdarshK15

Copy link
Copy Markdown
Member

/gcbrun

AdarshK15
AdarshK15 previously approved these changes Nov 1, 2026

@AdarshK15 AdarshK15 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, Please resolve pre-commit failures.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

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

@AdarshK15

Copy link
Copy Markdown
Member

/gcbrun

AdarshK15
AdarshK15 previously approved these changes Nov 5, 2026

@AdarshK15 AdarshK15 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

shubpal07
shubpal07 previously approved these changes Nov 6, 2026

@shubpal07 shubpal07 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 overall. The permission profiles, scoping rules, and example updates are clean and well-tested. Left a few minor, non-blocking suggestions on variable validation when settings are placed in top-level vars:.

@ep-nag
ep-nag dismissed stale reviews from shubpal07 and AdarshK15 via 8381d44 November 6, 2026 16:15

@shubpal07 shubpal07 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, thanks for addressing all the review comments!

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-new-modules Added to release notes under the "New Modules" heading.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants