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 — 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 — 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 per-organization federation.

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 — which principal to become, whose trust to present to — 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 per-organization federation 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 — replace 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. Independent of autoCreate on purpose: 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).

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 — your own cost-centre tagging, a cloud policy that appends tags — are left alone.

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 — 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 IAM-admin opt-in as any other provisioned identity (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 — 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, an opt-in on the onboarding assets; without it a workstation runs with no AWS identity at all.

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 honours 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 therefore leave it off by default, and it is an explicit opt-in. Without it, a CloudIdentity that declares a runtime identity refuses with a permission error rather than quietly booting a workstation with no identity. Turn it on only in an account dedicated to Ringleader workstations.

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

  • CloudAccount: the org-owned choice of which cloud identity to use, referenced by a federated identity.
  • Per-organization federation: the onboarding runbook for the recommended path.
  • Providers: how a workstation is placed, and what each provider’s providerConfig accepts.