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.

Your developers’ workstations can run as VMs in your own Google Cloud project, on your bill and inside your network. This page sets that up.

You run one Terraform module, or one shell script, in a project you own. It creates a service account for Ringleader to use, and tells Google to let Ringleader use it without a password or a key. You send a few values back to Ringleader, and from then on your developers create and delete workstations themselves. Ringleader never holds a key to your project, and never gets project admin.

What this creates in your project

Onboarding creates four things in your project, and you can see all of them in the Google Cloud console afterwards:

  • A service account called ringleader-workstations. This is the account Ringleader uses when it creates or deletes a VM.
  • Three roles and one custom role granted to that account. Together they let it create, start, stop and delete VMs, connect them to your network, and create service accounts for the machines it runs, and nothing else. The full list is below.
  • A workload identity pool with one provider. This is how Google confirms that a request really comes from Ringleader and is meant for your organization, without Ringleader ever holding a password or a key.
  • A network for the workstations: a VPC, a subnet, Cloud NAT and firewall rules. Terraform creates this unless you set create_network = false. On the gcloud path it is a separate script you run only if you want it.

What Ringleader can and cannot do in your project

There is no key to leak. Ringleader never receives a password or a key for your project, so there is nothing to rotate and nothing to steal. Each time it acts, it presents a short-lived signed token and Google checks it. Delete the pool and Ringleader loses access immediately.

Other Ringleader customers cannot reach your project. Every token names the organization it is for, and your project accepts only tokens that name yours. Google turns the rest away; that decision is made in your project, not by Ringleader.

Ringleader can only manage VMs. The service account holds three predefined roles and one custom role: no Owner, no Editor, nothing in billing.

  • roles/compute.instanceAdmin.v1: create, start, stop and delete VMs and their disks.
  • roles/compute.networkUser: attach a VM to your subnet.
  • roles/iam.serviceAccountUser: attach a service account to a VM. Every VM on Google Cloud runs as a service account, and attaching one is a separate permission that the two roles above do not include. Without it, creating the first workstation fails with a 403 error.
  • A custom role, Ringleader Managed Identities: create and delete service accounts, and nothing else. A VM on Google Cloud cannot start without a service account, and some machines Ringleader runs for you are not workstations, so it creates an account with no roles for each of those. This role cannot grant any role, so those accounts can do nothing in your project.

Three features are on by default and add to this: runtime identities, egress control and artifact storage. Each section says what it adds and how to turn it off.

What the workstations themselves can do

Ringleader’s own access stops at the roles above. The workstations are a separate question: every VM on Google Cloud runs as a service account, and whatever that account can do, the software on the VM can do too, including any AI coding agent your developers run there.

Unless you say otherwise, Google gives each workstation the project’s default Compute Engine service account. In many projects, especially older ones, that account has the Editor role, which can change almost anything in the project. You can check in the console under IAM: look for the account ending in compute@developer.gserviceaccount.com.

If the project exists only for Ringleader workstations, there is little for a workstation to reach and this is fine. If it holds anything else, give the workstations a service account with fewer roles; runtime identities shows two ways.

Before you start

You will have received two values from Ringleader when your organization was set up. If you do not have them, ask us.

ValueWhat it isExample
issuer URLThe address Ringleader’s tokens come 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 Google Cloud project to put the workstations in. We recommend a new, empty project used for nothing else. Workstations are billed to it and everything Ringleader may do is limited to it, so a dedicated project keeps the bill and the risk in one place. It is also the case where every default on this page is safe to leave alone.

In that project you need permission to manage service accounts and IAM: Owner, or Project IAM Admin plus Service Account Admin. You need it only for this setup, which you do once.

Set it up

Two paths that do the same thing, so use whichever your team already runs. Both live in github.com/ringleader-dev/cloud-onboarding, and both finish by printing the values you send back to Ringleader (see what you send back).

gcloud

The scripts are safe to run again if something goes wrong partway:

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 send back to Ringleader
./verify.sh

# optional, if you don't already have a network for the workstations.
# SSH_RANGES is where your engineers connect from, for an SSH rule of your own.
REGION=us-central1 SSH_RANGES=203.0.113.0/24 ./network-landing-pad.sh

Terraform

cd cloud-onboarding/gcp/terraform/examples/standalone
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars: your project id, the two values above, and
# ssh_source_ranges, the addresses your engineers connect from.
terraform init && terraform apply
terraform output handoff                        # the values to send back

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

What your project checks

When Ringleader asks to act in your project, Google checks three things about the token it presents. All three are built from the two values Ringleader gave you, and the module and the script configure them, so you never type them yourself.

What Google checksValueWhere it is configured
Who signed the token<issuer-url>/org/<org-id>the provider’s issuer_uri
Which organization it speaks fororg:<org-id>the provider’s attribute_condition
Which cloud it may be used against<issuer-url>/org/<org-id>/gcpthe provider’s allowed_audiences

If Google ever turns Ringleader away, compare these three against what was created. A mismatch in one of them is the usual cause, and a trailing slash on the issuer is the usual mismatch.

If you used the module or the script, that is all you need to know. If you built this into your own Terraform instead, check one more thing, because it is the common copy-paste mistake: who may use the ringleader-workstations service account (in the console, or with the third command under Verifying). It should be your organization alone, written principal://…/subject/org:<org-id>. If you see principalSet://…/workloadIdentityPools/<pool>/* instead, that is a wildcard that would let anyone the pool accepts use your service account.

Reaching your workstations

rl shell, rl tmux, port-forwards and VS Code Web all connect to the workstation over SSH on TCP 22, straight to the machine. A workstation behind an Edge is reached through a port on the edge instance instead.

Ringleader writes the firewall rule that lets you in. It puts a network tag of its own on every workstation it creates, and writes one VPC firewall rule admitting TCP 22 and 2222 to VMs with that tag, from any address unless the CloudAccount lists sshSourceRanges. A VM Ringleader did not create does not carry the tag, so the rule does not reach it. Writing the rule needs the egress control grant. An ingress deny rule of your own with a priority number of 900 or lower takes precedence over it, and so does a deny in a hierarchical firewall policy.

The module and the script can also write a rule of your own on TCP 22, for the addresses your engineers connect from:

ssh_source_ranges = ["203.0.113.0/24"]   # Terraform, in terraform.tfvars
SSH_RANGES=203.0.113.0/24 ./network-landing-pad.sh   # gcloud

That rule matches the ringleader-workstation network tag, which Ringleader puts on every workstation that declares no networkTags of its own. A workstation that declares its own list needs that tag in it. The rule applies alongside Ringleader’s, and it keeps workstations reachable from those addresses when Ringleader cannot write its own rule, for example with egress control turned off.

A workstation cannot fully confirm any of this for you. It sets itself up over its own outbound connection, so it reports Ready whether or not anything can reach it. When Ringleader could not write its rule, the workstation reports the SSHAdmissionMissing condition. Otherwise, open a shell to check.

External IP addresses

A workstation gets an external address by default, and with Ringleader’s rule in place that address accepts SSH connections from the internet, unless sshSourceRanges narrows it.

If you would rather none of your workstations had an external address, set allowPublicAddresses: false on the CloudAccount. To make it a default that developers can change per workstation, or to force it for the workstations one CloudIdentity builds, set it on the CloudIdentity instead:

spec:
  overrideProviderConfig:
    gcp:
      assignPublicIp: false

Use overrideProviderConfig when developers must not be able to turn it back on, and defaultProviderConfig when they may. Either way a workstation with no external address still reaches Ringleader through Cloud NAT, and you reach it over your VPN, Interconnect or peering.

Runtime identities (on by default)

Ringleader can give your developers their own Google Cloud identity to work as: it creates a service account per user, shared across that user’s workstations, and binds roles to it, so one person’s workstations can read one bucket and nobody else’s can (see runtime identity).

This is the one default on this page worth a deliberate decision, because creating and binding those accounts takes two more roles than the three above:

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

The second one is the reason to think about it. roles/resourcemanager.projectIamAdmin can grant any role in the project to anyone, including roles/owner. There is no narrower permission to give instead: the ability to set roles on a project is the ability to set any role.

So: in a project dedicated to Ringleader workstations, leave it on. In a project that holds anything else, turn it off when you apply:

enable_workstation_identities = false     # Terraform
WORKSTATION_IDENTITIES=0 ./onboard.sh     # gcloud

With it off, a workstation that asks for its own identity fails with a 403 error rather than silently getting nothing, and workstations run as the project’s default Compute Engine service account, as described above. Two ways to give them less:

  • Turn the feature back on, in a project that holds nothing but Ringleader workstations, and let Ringleader create a narrow account per developer.
  • Create one service account with no roles yourself and tell Ringleader to use it. This is how to get safe workstations in a shared project with the feature off: attaching an account Ringleader did not create needs only roles/iam.serviceAccountUser, which it already has. In the CloudIdentity, name the account in the serviceAccount block with roles: [] and autoCreate: false. The workstation starts exactly as before, and the software on it can do nothing in your project. See runtime identity for the fields.

Egress control (on by default)

By default a workstation can connect to anything your network can reach. Ringleader can narrow that to a list you choose per workstation, so a workstation can reach GitHub and your package registry and nothing else (see restricting outbound connections), enforced by VPC firewall rules Ringleader creates and keeps in step with the workstation’s configuration.

It is on by default. It also lets Ringleader write the firewall rule that admits SSH to your workstations, so with it turned off they are reachable only through a rule of your own. A workstation with no egress policy still has no limit on where it connects. Turn it off with enable_egress_control = false (Terraform) or EGRESS_CONTROL=0 (gcloud).

What it grants is a custom project role with fourteen permissions and nothing else:

PermissionsFor
compute.firewalls.create / delete / get / list / updatecreate and maintain the firewall rules that carry each policy, and the rule that admits SSH to your workstations
compute.routes.create / delete / get / listthe static route that sends traffic to the DNS/HTTPS proxy, for a policy that names hostnames rather than address ranges
compute.networks.updatePolicycreating that route additionally requires it
compute.addresses.create / delete / get / listreserving a fixed address for that proxy. Ringleader does not use them today: the proxy’s public address is not reserved, and it changes when Ringleader replaces the machine

Each distinct policy becomes one set of firewall rules, matched by network tag, so a hundred workstations sharing a policy cost one set of rules rather than a hundred.

What can defeat a policy here

The rules Ringleader writes are enough on their own to narrow a workstation. The one thing that can defeat them is a firewall rule of your own, because Google evaluates rules by priority and the lowest number wins:

PriorityRule
0–899Yours, deliberately left free so you can always override Ringleader in your own VPC
900the policy’s allowances, written by Ringleader
1000the policy’s default-deny, written by Ringleader
65535Google Cloud’s own implied allow-all egress, which the deny above exists to beat

An outbound allow rule of yours below 900 therefore wins over the deny, and the workstation reaches whatever that rule permits, while Ringleader still reports the policy as enforced, because it checks the rules it wrote and not yours. That is deliberate, so you can always override Ringleader in your own VPC. Know it before you write the first policy.

The network this module creates has no egress rule at all, so a VPC it built is clear. In a VPC you already had, check first:

gcloud compute firewall-rules list --project <project> --filter='direction=EGRESS' \
  --format='table(name, network.basename(), priority, disabled,
                  targetTags.list():label=TARGET_TAGS,
                  allowed[].map().firewall_rule().list():label=ALLOW)'

A policy also stops workstations reaching each other

If your developers run work that spans several machines, a policy stops those machines talking to each other unless you allow it. Name the workstation subnet range among the policy’s destinations and they keep talking.

The module’s allow_internal_traffic rule (on by default) lets workstations accept connections from each other, but a policy governs what a workstation may start, so that rule alone is not enough once a policy is in place.

Artifact storage (on by default)

This grant lets Ringleader store files for your organization in a Cloud Storage bucket in your project, instead of in Ringleader’s own storage, so the data stays in a project you control. Granting it writes nothing: Ringleader stores files there only once your organization is set up to use it.

It cannot widen its own access, it creates no long-lived keys, and it creates no bucket at apply time.

Two ways to take it, chosen by whether you name a bucket:

  • Leave artifact_storage_bucket empty and Ringleader creates and manages its own buckets, named ringleader-*. The grant is confined to that name pattern by an IAM condition, so it reaches no bucket you already have.
  • Name a bucket you created and Ringleader gets object access to that one bucket and nothing else, with no ability to create, change or delete a bucket. Its location, its lifecycle rules and its encryption key stay yours. Take this one if you have a data-residency or key-custody position to defend.

Turn it off with enable_artifact_storage = false (Terraform) or ARTIFACT_STORAGE=0 (gcloud). With it off, those files stay in Ringleader’s own storage, and nothing else changes.

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

terraform output handoff prints all of these together, and the gcloud path prints them when ./onboard.sh finishes.

Send them to whoever administers Ringleader for your organization, which may be you. They go into two Ringleader objects: a CloudAccount, which records which service account to use in your project, and a CloudIdentity, which says how workstations in your project are built (project, zone, machine type, network). See how Ringleader signs in to your cloud for a worked example of both.

Verifying

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

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

# The provider accepts only Ringleader's tokens, for your organization, for GCP:
gcloud iam workload-identity-pools providers describe oidc \
  --project <project> --location global --workload-identity-pool ringleader \
  --format='yaml(oidc.issuerUri, oidc.allowedAudiences, attributeCondition)'

# Your organization alone may use the service account (principal://, not principalSet://):
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 sent back, confirm Ringleader can use them before anyone creates a workstation. Replace gcp and dev with the CloudIdentity’s name and namespace:

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 Ringleader can use the identity. false means it cannot, and rl cloudidentity describe gcp -n dev says why. The usual causes are a wrong workload identity provider name or a wrong service-account email.

Revoking

Ringleader loses access the moment the pool is deleted. There are no keys to hunt down:

cd cloud-onboarding/gcp/gcloud
./revoke.sh              # deletes the 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.