CloudIdentity

The credential broker that lets the control plane create cloud VMs for a namespace.

A CloudIdentity brokers the credentials the control plane uses to create cloud machines for workloads in its namespace. It answers two separate questions: how Ringleader authenticates to your cloud, and what shape the workstations it governs take.

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity

It is admin-managed: only a namespace admin can author or change one.

How it is selected

When a Workstation routes to a cloud provider, the control plane picks the CloudIdentity in the same namespace whose spec.provider matches and whose spec.selector.matchLabels match the workstation’s labels. If several match, the highest priority wins (ties broken by name). An empty selector matches all workloads, so a high-priority catch-all can silently outrank scoped identities; keep catch-alls at priority: 0.

Exactly one identity is selected. They are not merged, so a high-priority, narrowly-scoped identity must carry everything the workstations it wins need, including its credential path, not just the field that makes it special.

Credential paths

A CloudIdentity takes exactly one credential path, chosen by which field it carries. Knowing which one you are on is the most important thing on this page.

PathSelected byWhat Ringleader presents to your cloud
Per-organization federation (recommended)spec.cloudAccountRefA short-lived assertion Ringleader signs, whose subject is your organization and which only your trust configuration accepts.
Impersonation (legacy)spec.impersonationA token from a Ringleader-owned intermediary identity that your account has granted impersonation rights to. The intermediary is shared across customers; prefer cloudAccountRef.
Ambientnone of the aboveThe worker’s own identity plus the deployment defaults. Valid, and the meaning of a provider-only CloudIdentity.

cloudAccountRef and impersonation are mutually exclusive. Federation presents a signed assertion and holds no cloud credential at all, so combining the two would be an object whose credential path depends on which field you read first. The control plane refuses it rather than resolving it by a precedence rule nobody can see:

apply cloudidentity/gcp: validation failed: spec: spec.cloudAccountRef and
spec.impersonation are mutually exclusive (cloudAccountRef selects the per-org
OIDC federation; impersonation selects the shared-intermediary chain; pick one)

There is no fallback between the paths. If a federated identity cannot mint, or the account is missing, the namespace belongs to no organization, the control plane has no federation configured, the workstation fails with a clear error. It never quietly reverts to another path.

spec.credentialRef is retired

A CloudIdentity once took a fourth, keyed path: spec.credentialRef named a Secret holding a long-lived cloud credential (a service-account JSON key on Google Cloud, an application client secret on Azure) which seeded the mint.

Both are gone. Ringleader now stores no cloud credential at rest on any path. Per-organization federation replaced them: a short-lived assertion, signed per mint and exchanged at your own cloud’s trust.

The field is refused at apply, not ignored, because an accepted-and-dropped credentialRef would leave you believing a workstation authenticates as the principal you pinned while it quietly minted through the deployment default instead. If you are carrying an old manifest, the apply names its replacement:

apply cloudidentity/azure: validation failed: spec.credentialRef: is no longer a
CloudIdentity field: ringleader stores no cloud credential at rest (the GCP
service-account key and the Azure keyed client secret are both retired). Use
spec.cloudAccountRef to name a CloudAccount. The per-org OIDC federation mints a
short-lived assertion exchanged at your own cloud's trust, with no stored secret.

Replace it with a CloudAccount and a cloudAccountRef; see how Ringleader signs in to your cloud.

Per-organization federation

This is the path to prefer for new cloud accounts. Ringleader signs a short-lived OpenID Connect assertion whose subject is the organization that owns the workstation’s namespace, derived server-side, and exchanges it directly at your cloud’s trust configuration. No Ringleader-owned intermediary identity is in the chain, and no credential of yours is stored anywhere.

Which cloud identity to use, meaning which principal to become and whose trust to present, lives on a separate, cluster-scoped CloudAccount that only an organization administrator can write. The CloudIdentity just points at it by name:

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity
metadata:
  name: gcp
  namespace: dev
spec:
  provider: gcp
  selector:
    matchLabels:
      cloud: gcp
  # The credential EDGE (which principal, whose trust) lives on the CloudAccount.
  # The machine SHAPE (project / zone / machineType) stays here.
  cloudAccountRef: acme-gcp
  defaultProviderConfig:
    gcp:
      project: acme-dev
      zone: us-east4-c
      machineType: c4-standard-4

Azure is the same shape: the account carries the Entra application and the tenant, the identity carries the placement:

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity
metadata:
  name: azure
  namespace: dev
spec:
  provider: azure
  selector:
    matchLabels:
      cloud: azure
  cloudAccountRef: acme-azure
  defaultProviderConfig:
    azure:
      subscriptionId: 8d6e5f6e-5090-4221-9eae-cc956b238590
      resourceGroup: acme-workstations
      location: eastus
      subnetId: /subscriptions/…/virtualNetworks/acme-vnet/subnets/workstations
      publicIp: true

AWS is the same shape: the account carries the IAM role to assume, the identity carries the placement:

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity
metadata:
  name: aws
  namespace: dev
spec:
  provider: aws
  selector:
    matchLabels:
      cloud: aws
  cloudAccountRef: acme-aws
  defaultProviderConfig:
    aws:
      region: us-east-1
      instanceType: m6i.xlarge
      subnetId: subnet-0abc123
      securityGroupIds: [sg-0abc123]
      assignPublicIp: true

The referenced account must belong to the same organization as this CloudIdentity’s namespace; a cross-organization reference is refused when you apply it. See CloudAccount for the account object, and how Ringleader signs in to your cloud for the trust configuration to create in your cloud.

Impersonation (legacy)

Do not onboard a new account this way

This is the older path, kept only so existing accounts keep working while they migrate. Use cloudAccountRef instead.

It works by having Ringleader’s own identity impersonate an intermediary it owns, which in turn impersonates the target principal in your account. No secret is stored, but the intermediary is shared across every customer, so your account must trust an identity that other tenants can also name. Per-organization federation exists precisely to remove that: the trust is pinned to a signed claim carrying your organization’s id, and nothing else can present it.

Ringleader’s own identity impersonates an intermediary it owns, which in turn impersonates the target principal in your account. No secret is stored, but the intermediary is shared, so your account must trust it.

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity
metadata:
  name: gcp
  namespace: dev
spec:
  provider: gcp
  selector:
    matchLabels:
      cloud: gcp
  impersonation:
    intermediaryPrincipal: ringleader-iam@example-iam.iam.gserviceaccount.com
    targetPrincipal: ringleader-workstations@acme-dev.iam.gserviceaccount.com

Azure, keyless, over the same chain: targetPrincipal is the Entra application’s client id, and that application carries a federated identity credential trusting Ringleader’s intermediary:

spec:
  provider: azure
  selector:
    matchLabels:
      cloud: azure
  impersonation:
    intermediaryPrincipal: ringleader-iam@example-iam.iam.gserviceaccount.com
    targetPrincipal: 00000000-0000-0000-0000-000000000000   # the Entra app client id
  overrideProviderConfig:
    azure:
      subscriptionId: 22222222-2222-2222-2222-222222222222
      resourceGroup: ringleader-workstations
      location: eastus
      subnetId: /subscriptions/…/virtualNetworks/ringleader-vnet/subnets/workstations

On this path the Azure tenant is a control-plane-wide setting, not something the CloudIdentity carries, which is precisely one of the things federation fixes: a CloudAccount holds the tenant per-customer, so two customers in two tenants become expressible.

Migrating an identity from impersonation to federation is a one-line edit, replacing the impersonation block with a cloudAccountRef, and it reverses the same way, per identity, with no coordinated all-at-once switch.

Spec fields

FieldTypeDescription
providerstringRequired. The backend the credentials target (gcp, azure, aws).
priorityintPrecedence when several match (default 0); higher wins.
selector.matchLabelsmapWhich workloads this identity brokers for. Empty matches all.
cloudAccountRefstringThe cluster-scoped CloudAccount that specifies which cloud identity this uses. Selects per-organization federation. Mutually exclusive with impersonation.
impersonation.intermediaryPrincipalstringIntermediary the control plane’s own default identity impersonates (hop 1).
impersonation.targetPrincipalstringTarget principal in the tenant (hop 2). GCP: a service-account email; Azure: the Entra app client id.
impersonation.targetScopestringAdditional provider-specific scope.
serviceAccount.accountIdstringDeclares a runtime identity the workstation VMs run as (see below). GCP: the service-account id; Azure: the user-assigned managed-identity name.
serviceAccount.displayNamestringHuman label for the runtime identity (GCP displayName; advisory on Azure).
serviceAccount.rolesarrayExact-set role grants for the runtime identity: {role, resource} pairs. GCP: roles/… or a custom role id, bound at project scope (see below); Azure: a built-in role name, GUID, or role-definition id, with resource an ARM scope id (empty: the identity’s resource group).
serviceAccount.autoCreateboolDoes Ringleader create the runtime identity? Default true. Set false when your own infrastructure-as-code pre-creates it: Ringleader then only adopts it, and an absent one fails the workstation loudly rather than being conjured without the grants it exists to carry.
serviceAccount.autoDeleteboolDoes Ringleader delete the runtime identity when the last workstation using it goes away? Default true. Set false so tearing down a workstation cannot destroy an identity your own tooling declares, and with it any grants attached elsewhere. It is independent of autoCreate, so auto-create plus never-delete is a legitimate combination.
defaultProviderConfigobjectPer-provider providerConfig defaults (workstation overrides these).
overrideProviderConfigobjectPer-provider providerConfig forced by policy (wins over the workstation).
subnetstringPlaces every workstation this CloudIdentity selects in this subnet. It also claims the subnet for this namespace, so no other namespace in the organization can place a workstation in it. See Claiming a subnet.

Everything a CloudIdentity declares is validated when you apply it, not when a workstation later tries to boot. This object is a template for other people’s workstations, so a typo in it would otherwise surface as an opaque cloud error on someone else’s machine.

Shaping the workstations it brokers

defaultProviderConfig and overrideProviderConfig mirror a Workstation’s own spec.providerConfig (a per-provider map) and fold around it:

defaultProviderConfig  ◁  the workstation's own providerConfig  ◁  overrideProviderConfig

A default supplies a value a workstation may override (a fleet-wide machine type). An override forces one regardless of what the workstation authored (a project and zone pinned by policy).

Both blocks may also carry the VM’s cloud labels/tags, and those values may be templated per workstation (owner: ${shortHash:creatorUserId}, team: ${metadata:namespace}) plus an addRingleaderLabels: true shorthand for the standard Ringleader-owned set. They are expanded once per governed workstation and sanitized to one portable character set, so the same manifest produces the same label on every cloud.

Put an identity label in overrideProviderConfig, never in defaultProviderConfig: a default is by definition overridable by the workstation’s own providerConfig, so a member could relabel their workstation as someone else. An override cannot be.

Ringleader only ever removes labels it applied itself. Keys put on a VM by anything else, such as your own cost-center tagging or a cloud policy that appends tags, are left alone.

Claiming a subnet

spec.subnet names a subnet on GCP, Azure or AWS. It has two effects:

  • Every workstation this CloudIdentity selects is placed in that subnet. The value replaces any subnet the workstation’s own providerConfig names.
  • The subnet is claimed for this CloudIdentity’s namespace. A workstation from another namespace in the organization that would be placed there is refused when it is applied. That refusal does not name the namespace that owns the subnet.

A subnet nobody has claimed stays open to every namespace. Workstations already in a subnet when it is claimed can still be edited.

Name the subnet in its cloud’s full form:

  • GCP: projects/<project>/regions/<region>/subnetworks/<name>, or its self-link URL. A bare name is refused, because two projects can each have a subnet called default.
  • Azure: the subnet’s full resource id.
  • AWS: the subnet-… id.

Name the subnet in spec.subnet alone. A CloudIdentity that also names one in defaultProviderConfig or overrideProviderConfig, under subnetwork on GCP or subnetId on Azure and AWS, is refused:

spec.subnet and spec.defaultProviderConfig.gcp.subnetwork both name a subnet: remove spec.defaultProviderConfig.gcp.subnetwork

The namespace must belong to an organization. In a namespace with no organization, a CloudIdentity that sets spec.subnet is refused. Name the subnet in providerConfig there instead.

A subnet can be claimed by one namespace. A second CloudIdentity naming the same subnet from another namespace is refused, and that refusal names the namespace that owns it.

Claiming matters most on AWS and Azure, where an Edge routes a whole subnet. An Edge does not start routing a subnet while a machine it does not serve is in it. A machine that another namespace creates there later is routed too, and it loses its outbound access, because the Edge holds no rule for it. A claim keeps other namespaces from placing workstations there.

Runtime identity (spec.serviceAccount)

Everything above is about the identity Ringleader authenticates to the cloud with. spec.serviceAccount is a different axis: the identity each matching workstation VM runs as. Without it, VMs are booted with no attached service account or managed identity, so nothing inside the workstation can act as any cloud principal, with one exception on Google Cloud, described just below. With it, the control plane provisions a dedicated identity (a GCP service account, or an Azure user-assigned managed identity), grants it exactly the declared roles, and attaches it to the VM, so an agent working in the workstation holds its own scoped, revocable identity, never the operator’s.

A GCP cloud VM always gets an attached SA, so make it a permission-less one

A GCP cloud workstation must present a Google-signed instance-identity assertion to come up, and Google mints that assertion only for a VM that has an attached service account. So a GCP workstation with no service account of its own falls back to the project’s default compute service account, which on most projects is broadly privileged. The result: a GCP workstation that declares no serviceAccount runs as a broad identity purely so it can start.

Whether the workstation can start only ever depends on Google’s signature and the instance’s identity claims (project, zone, instance name); it never looks at the service account’s IAM roles. So a zero-role service account works exactly the same and lets the workstation act as nothing. To get least privilege on GCP, name a permission-less identity:

spec:
  provider: gcp
  cloudAccountRef: acme-gcp
  defaultProviderConfig:
    gcp:
      project: acme-dev
      zone: us-east4-c
  serviceAccount:
    accountId: rl-ws-default   # a service account with NO role bindings
    roles: []                  # least privilege: Ringleader binds nothing
    autoCreate: false          # your IaC pre-creates the zero-role SA; Ringleader ADOPTS it
    autoDelete: false          # Ringleader never deletes your SA

Set autoCreate: true instead if you would rather Ringleader mint the zero-role SA for you; that needs the same onboarding grant as any other provisioned identity, which is on by default but is the one an operator may have opted out of (see the note further down). Either way the SA holds no roles, so the workstation starts yet can reach nothing.

apiVersion: core.ringleader.dev/v1
kind: CloudIdentity
metadata:
  name: gcp-bucket-read-only
  namespace: dev
spec:
  provider: gcp
  priority: 100
  selector:
    matchLabels:
      ws-bucket: read-only
  cloudAccountRef: acme-gcp
  defaultProviderConfig:
    gcp:
      project: acme-dev
      zone: us-east4-c
      machineType: c4-standard-4
  serviceAccount:
    accountId: ws-ro-${shortHash:creatorUserId}     # one identity per user
    displayName: "Workstation bucket read-only (per user)"
    roles:
      - role: projects/acme-dev/roles/wsBucketReadOnly

A workstation opts in by carrying the label:

metadata:
  labels:
    ws-bucket: read-only
spec:
  requirements: [provider:gcp]

Because identities are selected and not merged, the higher-priority object above carries the full credential path (cloudAccountRef) and placement as well as the serviceAccount block, since otherwise a workstation that matched it would win an identity with no way to create a VM at all.

Supported for gcp and azure. The declared roles are kept as an exact set: a role removed from the spec is revoked in the cloud on the next update cycle. Templating accountId with ${shortHash:creatorUserId} gives each user their own runtime identity, shared across that user’s workstations.

On AWS the analogue is an EC2 instance profile you name with providerConfig.aws.iamInstanceProfile (Ringleader attaches, but does not create, the profile). Attaching one needs iam:PassRole, which the onboarding assets grant by default, bounded to one IAM path; where an operator has opted out, a workstation naming a profile fails to launch.

On Google Cloud, roles are bound at project scope today. A role entry with a non-empty resource is not yet bound, and is reported on the resulting CloudServiceAccount’s status rather than silently applied. Scope the role itself (a custom project role over just the resources you mean) until per-resource bindings land. Azure honors resource as an ARM scope id.

Your cloud account must be onboarded for this

Provisioning an identity and binding roles to it costs real permissions in your account: on Google Cloud, the power to administer project IAM; on Azure, the power to create role assignments (which built-in Contributor does not even have). The onboarding assets grant them on by default, so a pad applied with the defaults can already do this. That is why the onboarding asks for an account dedicated to Ringleader workstations, and why an operator running it somewhere shared should opt out. Where they have, a CloudIdentity that declares a runtime identity refuses with a permission error rather than quietly booting a workstation with no identity.

Two operational notes:

  • The provisioned identity is tracked as a CloudServiceAccount (alias csa), a control-plane bookkeeping record you can inspect with rl csa get but never author yourself. It is owner-referenced by the workstations that use it and garbage-collected (including the real cloud identity and its role grants) once they are all gone.
  • Changing the runtime identity of a running GCP workstation requires a stop. GCE only allows attaching or swapping a VM’s service account while the instance is stopped, so the controller applies such a change through an automatic stop, apply, and start cycle rather than in place. Expect a brief restart of the workstation.

Status fields

FieldTypeDescription
validboolWhether the identity is well-formed and usable.
messagestringOn success, the provider; on failure, the validation/broker error (plus advisory warnings).
lastVerifiedstringRFC3339 timestamp of the last verification (re-checked on a periodic cadence, and immediately after you edit the spec).

See also