Onboarding Google Cloud

Grant Ringleader least-privilege, keyless access to run workstation VMs in one of your GCP projects, with Terraform or the gcloud CLI.

Let a Ringleader control plane run Workstation VMs in one of your GCP projects using the minimum permissions, no static keys, and no project admin — by trusting Ringleader’s OIDC issuer through Workload Identity Federation.

The model

Ringleader control plane
  │  signs a short-lived OIDC token:
  │    iss = <issuer>/org/<your-org-id>,  sub = org:<your-org-id>
Workload Identity Pool + OIDC provider in YOUR project
  │  admits ONLY sub = org:<your-org-id>, and only the per-org audience
ringleader-workstations@<your-project>   (the service account you create)
  │  roles/compute.instanceAdmin.v1  +  roles/compute.networkUser  (project-scoped)
creates / manages / deletes workstation VMs in YOUR project
  • Keyless. No service-account key is created. Ringleader exchanges its signed token for a short-lived federated token and impersonates your service account. Delete the pool and access stops.
  • Pinned to your organization. The provider trusts Ringleader’s issuer and requires the token’s subject to be exactly your organization. A token minted for any other Ringleader customer carries a different subject and is refused at the token exchange. The impersonation grant is bound to that single subject — never a pool-wide wildcard.
  • Least privilege. The service account holds only roles/compute.instanceAdmin.v1 (instances and disks) and roles/compute.networkUser (attach a NIC to your subnets). No roles/owner, no roles/editor, no IAM, billing, or storage access. Separately, the workstation VMs it creates run as your project’s default Compute Engine service account unless you set up a dedicated one — the same identity Compute Engine gives any VM by default, and usually a broadly privileged one. If you want software on a workstation to have less access than that, configure a dedicated, scoped service account (see runtime identities).

Before you start

Ringleader gives you two values:

ValueWhat it isExample
issuer URLThe origin Ringleader signs its tokens from. No trailing slash.https://oidc-app.ringleader.dev
organization idYour organization’s id — a UUID, never its name.0192f5bf-af83-7178-8d0a-f1c7aea06bde

You also need a project to place workstation VMs in (your billing/RBAC boundary), and rights to manage service accounts and IAM in it (Owner, or Project IAM Admin + Service Account Admin) — a one-time human step.

Apply the assets

Both paths live in github.com/ringleader-dev/cloud-onboarding.

gcloud

Scripts safe to re-run:

git clone https://github.com/ringleader-dev/cloud-onboarding
cd cloud-onboarding/gcp/gcloud

export PROJECT=my-company-dev-workstations
export ISSUER_URL='https://oidc-app.ringleader.dev'
export ORG_UID='0192f5bf-af83-7178-8d0a-f1c7aea06bde'

./onboard.sh          # prints the values to hand back to Ringleader
./verify.sh

# optional, if you don't already have a subnet:
REGION=us-central1 ./network-landing-pad.sh

Terraform

cd cloud-onboarding/gcp/terraform/examples/standalone
cp terraform.tfvars.example terraform.tfvars   # then edit
terraform init && terraform apply
terraform output handoff

The module is reusable — reference it as a module source from your own configuration rather than copying it, if you prefer.

What the trust pins

These values are derived from the two you were given, and are what make the trust reach your organization and nothing else. The assets set them for you; this is what to check if a mint is ever refused.

PinValue
Issuer (issuer_uri)<issuer-url>/org/<org-id>
Subject (assertion.sub)org:<org-id>
Audience (allowed_audiences)<issuer-url>/org/<org-id>/gcp

Two rules matter, and the assets follow both:

  • The provider sets an attribute_condition of assertion.sub == 'org:<org-id>'.
  • roles/iam.workloadIdentityUser is bound to the exact subject (principal://…/subject/org:<org-id>), never to a principalSet://…/workloadIdentityPools/<pool>/* wildcard. The wildcard is the common copy-paste, and it would let any subject in the pool impersonate your service account.

Reaching your workstations

Ringleader has no bastion and no SSH tunnel: rl shell, rl tmux, port-forwards and VS Code Web all dial the workstation on TCP 22. Bringing it up needs only egress, so it is entirely possible to end up with a VM that comes up, reports Ready, and that nobody can open.

On GCP a workstation gets an external IP by default (opt out per workstation with providerConfig.gcp.assignPublicIp: false) — but a custom VPC has no firewall rules and GCP denies ingress by default, so the workstation is still unreachable until you allow 22:

create_network    = true
ssh_source_ranges = ["203.0.113.0/24"]   # the CIDRs your engineers connect from

That creates a single rule targeting the ringleader-workstation network tag, so it applies to your workstations and to nothing else in the VPC. Put the same tag on the workstations:

spec:
  providerConfig:
    gcp:
      networkTags: [ringleader-workstation]

Leave ssh_source_ranges empty only if you reach the subnet privately (VPN, Interconnect, peering) from wherever you run rl.

Optional: workstations that run AS an identity

Ringleader can boot each workstation as a dedicated service account it provisions per user and binds roles to — see runtime identity. It is off by default, because it is not free:

CapabilityRole it needs
Create/delete the per-user service accountroles/iam.serviceAccountAdmin
Bind roles to it on the projectroles/resourcemanager.projectIamAdmin
Attach it to a VMroles/iam.serviceAccountUser

roles/resourcemanager.projectIamAdmin can grant any role in the project to any principal — including roles/owner. That is inherent: setting a role binding is project-IAM administration. Enable it (enable_workstation_identities = true) only in a project dedicated to Ringleader workstations. Left off, the feature refuses with a 403 rather than quietly working.

Note that “off” does not mean a workstation runs as nobody. A GCP cloud workstation must present a metadata-signed instance identity to come up, which Google mints only for a VM that has an attached service account — so with this feature off and no serviceAccount named, each workstation runs as your project’s default Compute Engine service account, typically a broad one.

You have two ways to narrow it:

  • A dedicated, role-bound identity — this feature. It needs the projectIamAdmin grant above (to bind roles), so keep it in a Ringleader-only project.
  • A permission-less identity — a service account with no roles, named in the CloudIdentity serviceAccount block with roles: []. The workstation attaches it and comes up exactly the same (whether it can start never checks the SA’s roles) while able to act as nothing. If your own IaC pre-creates it and you set autoCreate: false, Ringleader only adopts it, so this needs just roles/iam.serviceAccountUser (to attach) — not serviceAccountAdmin or projectIamAdmin. See runtime identity.

What you hand back to Ringleader

ValueWhere it lands
target service account emailCloudAccount spec.gcp.targetServiceAccount
workload identity provider (the resource name)spec.gcp.workloadIdentityProvider
project idCloudIdentity providerConfig.gcp.project
subnetwork self-link (only if you created a network)providerConfig.gcp.subnetwork

The first two become which cloud identity to use; the rest become the VM’s size and placement. See per-organization federation for the objects Ringleader builds from them.

Verifying

./verify.sh checks all of this for you. By hand:

# The service account holds exactly the two expected roles:
gcloud projects get-iam-policy <project> \
  --flatten='bindings[].members' \
  --filter="bindings.members:ringleader-workstations@<project>.iam.gserviceaccount.com" \
  --format='table(bindings.role)'

# The OIDC provider pins your issuer, audience and subject:
gcloud iam workload-identity-pools providers describe oidc \
  --project <project> --location global --workload-identity-pool ringleader \
  --format='yaml(oidc.issuerUri, oidc.allowedAudiences, attributeCondition)'

# The impersonation binding names an EXACT subject, not a principalSet wildcard:
gcloud iam service-accounts get-iam-policy \
  ringleader-workstations@<project>.iam.gserviceaccount.com --project <project>

Once an administrator has created the CloudAccount and CloudIdentity from the values you handed back, confirm the Ringleader side resolves your trust before anyone tries to boot a workstation:

rl wait cloudidentity gcp -n dev --for Ready --timeout 2m
rl cloudidentity get gcp -n dev -o yaml     # status.valid: true

status.valid: true means the identity is usable — the account resolves and the cloud identity it selects can be used. false points at the workload identity provider or the service-account email; rl cloudidentity describe gcp -n dev says which.

Revoking

Access stops the moment the trust is gone — there are no keys to chase:

cd cloud-onboarding/gcp/gcloud
./revoke.sh              # deletes the federation pool, keeps the service account
FULL=1 ./revoke.sh       # also deletes the service account

Terraform: terraform destroy.

A deleted pool is soft-deleted for 30 days

Workload identity pool ids stay reserved after deletion. If you revoke and re-onboard within that window, undelete the pool rather than creating it again: gcloud iam workload-identity-pools undelete ringleader --location global. The scripts already handle this.