ServiceAccount
A non-human identity your CI or automation becomes — with no stored secret: it trusts an external issuer, not a password.
A ServiceAccount is an identity that is not a person: the thing a CI pipeline, a scheduled job, or a piece of automation signs in as. You create it, scope it with ordinary RBAC, audit it, and revoke it.
apiVersion: core.ringleader.dev/v1
kind: ServiceAccountThe important part is what it does not hold: there is no password, no API key, no long-lived token stored anywhere. A service account holds a trust list — which external identities are allowed to become it. A GitHub Actions job proves who it is using the identity token GitHub already mints for it, Ringleader checks that proof against the trust list, and issues a short-lived token in return. Nothing to leak, nothing to rotate, nothing to paste into a secrets store.
Its subject is sa:<namespace>/<name> — always namespace-qualified, so two teams can
both have a ci without them being the same identity anywhere.
The two objects
Inbound federation takes two objects, authored at two levels:
| Object | Scope | Who authors it | What it says |
|---|---|---|---|
IdentityProvider | cluster-scoped, belongs to one org | an org-admin | “tokens signed by this issuer may become identities here” — the trust anchor |
ServiceAccount | namespaced | anyone who can create one in the namespace | “these specific external identities may become me” |
The split matters: registering an issuer is a tenancy decision for the whole organization, while deciding that one particular repository may become one particular robot is a day-to-day decision inside a namespace.
IdentityProvider
The trust anchor. It names the external issuer and the ceiling of what a trust rule may pin.
apiVersion: core.ringleader.dev/v1
kind: IdentityProvider
metadata:
name: acmecorp-github # must be, or start with, the owning org's name
spec:
org: acmecorp
profile: github-actions
allowClaims: ["environment", "job_workflow_ref"]| Field | Type | Description |
|---|---|---|
org | string | Required. The owning organization. |
profile | string | Required. The ecosystem this provider represents: github-actions, gitlab-ci, gcp, aws, azure, or custom. |
issuer | string | The issuer URL. Required for custom, and for any profile whose issuer is per-tenant or per-instance (self-hosted GitLab, Azure, an EKS cluster). Fixed by the profile otherwise — a conflicting value is refused rather than silently overridden. |
audience | string | The audience a trust rule may name. Optional: the deployment already publishes the audience it answers to. A value here may only narrow within what the deployment accepts. |
requireClaims | []string | Extra claims every rule under this provider must pin, on top of the profile’s own mandatory ones. It can only tighten. Required (non-empty) for custom. |
allowClaims | []string | Claims a rule may pin beyond the mandatory ones. It never restates the mandatory claims. |
enabled | bool | Master switch for the whole trust anchor. Absent means enabled; false refuses every exchange through it. |
Two rules to know before you write one:
- Its name must be the owning org’s name, or start with it followed by a hyphen.
The kind is cluster-scoped, so every org wanting a provider called
githubwould collide:acmecorp-githuboracmecorp, never a baregithub. - Only an
org-admincan author one. A namespace member has no access to it at all. If you signed up on your own, you already administer your personal organization, so you can register one for yourself — see Organizations.
Profiles
A profile records one ecosystem’s issuer, how audiences behave there, and — most importantly — the claims a rule must pin to name a stable identity. You cannot pin fewer; you may always pin more.
| Profile | A rule must pin | Notes |
|---|---|---|
github-actions | repository_owner_id and repository_id | Harden further with a GitHub Environment that has required reviewers (the environment claim) and/or job_workflow_ref for a reusable workflow. |
gitlab-ci | namespace_path or project_path | Harden with a protected environment (environment + environment_protected) and/or ref_protected. |
gcp | email or sub | The audience is chosen by the caller on the metadata server’s identity endpoint. |
aws | sub | The audience is set on the projected service-account token. |
azure | tid, and oid or sub | The audience is a registered resource rather than a URL of ours, so the org registers which resource value the deployment accepts. |
custom | nothing built in — so requireClaims must name at least one claim | Also requires an explicit issuer. |
Run rl serviceaccount trust --help for the same table as your deployment sees it.
ServiceAccount
apiVersion: core.ringleader.dev/v1
kind: ServiceAccount
metadata:
name: ci
namespace: dev
spec:
description: Builds and tests the platform repo
trust:
- provider: acmecorp-github
match:
repository_owner_id: "1234567"
repository_id: "7654321"
environment: production| Field | Type | Description |
|---|---|---|
description | string | What this identity is for. Worth writing — it is what a reader sees months later. |
enabled | bool | Master switch. Absent means enabled; false refuses every sign-in immediately, without deleting the account or its permissions. |
trust | []object | The trust list. See below. |
Trust rules
Each entry of spec.trust is one external identity allowed to become this account.
| Field | Type | Description |
|---|---|---|
provider | string | Required. The IdentityProvider this rule trusts. |
audience | string | Optional. Narrows the accepted audience for this rule within what the deployment accepts. |
match | object | Required. The claims this rule pins, as exact values. |
The matching rules are deliberately blunt:
- Every claim in a rule must match (AND) for that rule to admit.
- Any rule matching admits (OR), so several pipelines can share one account.
- Matching is exact string equality. No prefixes, no wildcards, no regular expressions — an authority boundary is not a place for pattern matching.
Add a rule with the CLI rather than by hand if you like — a rule that pins too few claims for its provider’s profile is refused either way:
rl serviceaccount create ci
rl serviceaccount trust ci --provider acmecorp-github \
--claim repository_owner_id=1234567 \
--claim repository_id=7654321 \
--claim environment=productionGiving it permissions
A service account holds no rights of its own. Rights come from ordinary role bindings naming its subject, exactly as they would for a person:
apiVersion: core.ringleader.dev/v1
kind: RoleBinding
metadata:
name: ci-can-build
namespace: dev
spec:
subjects: ["sa:dev/ci"]
roleRef:
kind: GlobalRole
name: namespace-memberYou can never bind an account to more than you hold yourself, so an account is bounded by its author’s own access — before and after it is created.
Status fields
The system writes these.
| Field | Type | Description |
|---|---|---|
lastAssumedAt | string | When something last signed in as this account. Absent if nothing ever has. |
lastAssumedBy | object | Who did: {provider, ruleIndex, claims} — the trust anchor, which rule admitted them, and the identity claims worth showing. |
authorityReason | string | Why the account has been suspended, when it has. Absent on a healthy one. |
conditions | []object | Carries Unused — see below. |
The Unused condition
Every account publishes an Unused condition, answering “should someone look at
this?”. Its reason says which of four situations it is:
| Reason | Status | Meaning |
|---|---|---|
RecentlyUsed | False | Used within the last 30 days. Healthy. |
NotYetUsed | False | Never used, but created within the last 30 days — a pipeline that has not run yet looks exactly like this, so it is not flagged. |
NotUsedRecently | True | Used, but not for over 30 days. |
NeverUsed | True | Never used at all, and created over 30 days ago — the most suspicious of the four: an identity nothing has ever needed, still usable. |
Flagging is all it does. Nothing is ever auto-disabled or auto-deleted at any
threshold — a quarterly release pipeline is not abandoned, it is quarterly. When an
account genuinely is finished, set enabled: false or delete it.
The window is a fixed 30 days. It is not configurable, per account or per deployment: a threshold the owner of a forgotten account could raise until the flag stopped firing would not be worth publishing.
Usage is *recorded*, not inferred
Suspension
An account is suspended when the person who created it no longer has permission to
create one in that namespace. It stops being usable immediately, without being deleted,
and status.authorityReason says so in as many words. An administrator can restore the
creator’s access, or the account can be recreated by someone who does have it.
This is checked continuously, not just at creation — which is what makes “a service account never carries more than its creator could grant directly” stay true after the fact.
Listing and inspecting
rl serviceaccount get (alias sa) answers “which identities is nothing using?”
directly:
rl serviceaccount getNAME LAST USED STATUS AGE
ci 14m Ready 62d
nightly 41d Unused 95d
old-robot NEVER Unused 210d
legacy-ci 3d Disabled 1y- LAST USED is how long ago something last signed in, or
NEVER. - STATUS is one word, most-blocking first:
Suspended(the platform withdrew it),Disabled(you switched it off withenabled: false),Unused(the condition above is flagged), elseReady.
rl serviceaccount describe ci adds the full status, including who last assumed the
account and under which rule. Sign-ins are also recorded as events in the namespace,
so rl event get shows an Assumed entry naming the provider and rule — and an
AssumptionRefused warning when something proved a real identity from a provider your
org registered but matched none of your rules, which is usually a rule that needs
updating.
Signing in as one
From a CI job that has an ambient workload identity, log in with no secret at all:
# .github/workflows/build.yml
permissions:
id-token: write # required — this is what lets the job mint its own assertionrl auth login https://ringleader.example.com \
--service-account dev/ci \
--provider acmecorp-githubThe CLI asks the job’s platform for an identity assertion, presents it, and gets a short-lived token back. Nothing is stored on disk that is worth stealing: what it keeps is the recipe (which origin, which provider, which account), which is useless without an ambient identity only that runner can produce.
| Flag | Description |
|---|---|
--service-account <ns>/<name> | Log in as this account by exchanging the job’s own workload identity. |
--provider <name> | Required with --service-account. The IdentityProvider to exchange against. |
--audience <value> | The audience to request the assertion for. Defaults to the audience the control plane advertises for itself, so you normally never set this — pass it only for a migration, or a hostname the operator accepts in addition to the canonical one. |
--register-node | Also register this device’s node identity. Off by default: an ephemeral runner’s registration would add its key to every workstation the account owns and re-push their configuration. Turn it on for a long-lived robot on a fixed host. |
This only works where an ambient workload identity exists. On a laptop there is nothing
to exchange, so use the ordinary rl auth login.
A refusal never says why
Turning one off
Three levers, from narrowest to widest:
- Delete a trust rule — that external identity can no longer become the account; the others still can.
spec.enabled: falseon the account — nothing can become it, but the object and its permissions survive, ready to be switched back on.spec.enabled: falseon theIdentityProvider— every account trusting that issuer stops working at once. The break-glass switch for a compromised issuer.
Deleting the account removes it entirely. All four take effect on the very next sign-in attempt, not on a cache’s schedule.
See also
- Access control — the roles you bind an account to.
- Organizations — who may register an
IdentityProvider. rl auth— login, status, and tokens.