Organizations
The tenancy layer above namespaces: a company, its verified domain, how people join it, and who administers it.
An Org is the tenancy layer above namespaces: one company, with its namespaces as its teams and business units.
apiVersion: core.ringleader.dev/v1
kind: OrgA namespace stays the unit of isolation — RBAC, secrets, resources all scope to it. But the company-to-team relationship is real, and an Org is what records it. Without one, five business-unit namespaces are just five unrelated namespaces, and nothing says they are one customer.
Making it explicit is what lets Ringleader offer:
- A billing and plan entity — the one thing a namespace cannot be.
- Self-serve team formation. A verified
@acme.comorg auto-joins the next Acme employee, instead of stranding them in a lonely personal namespace. - An administrator with reach. One
org-admingrant covers every namespace of the org, present and future — no per-namespace bootstrap. - Cloud federation. A CloudAccount belongs to an org, and the assertion Ringleader signs for your cloud carries the org’s id as its subject.
The shape
The grouping is modeled child to parent: every namespace points at its org, and an org never holds a list of its namespaces.
┌──────────┐
│ Org │ acmecorp plan, autoJoin, defaultNamespace
└────┬─────┘
│ referenced by spec.org
┌─────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
Namespace Namespace OrgDomain CloudAccount
acmecorp acmecorp- acme.com acme-gcp
team-a (verified) (cluster-scoped)A namespace rolls up to at most one org. A namespace with no org still works for everything local to it, but it cannot broker a federated cloud mint — there is no organization for Ringleader to sign an assertion for, and it refuses rather than guessing one.
Org spec
apiVersion: core.ringleader.dev/v1
kind: Org
metadata:
name: acmecorp
spec:
displayName: Acme Corporation
plan: team
autoJoin: true
defaultNamespace: acmecorp| Field | Type | Description |
|---|---|---|
displayName | string | Human label. |
plan | string | free, team, or enterprise. |
suspended | bool | Hard stop (non-payment, abuse). Consumers gate on it. |
autoJoin | bool | Whether someone with an email at a verified domain of this org joins it automatically. |
defaultNamespace | string | The shared namespace new members land in. Unset: a namespace named after the org. |
billingAccountRef | string | A reference into the external billing system of record — never a ledger. |
maxNamespaces | integer | How many namespaces this org may hold. Minimum 1; defaults to 50. |
settings | object | Free-form bag for opaque data. The control plane never branches on its contents. |
Orgs are cluster-scoped and created by Ringleader operators. An org-admin
reads their own org but does not rewrite its plan or suspension state; what they
do administer is everything under it (see Who administers an
org).
The namespace cap
spec.maxNamespaces bounds how many namespaces roll up to one org. A write that
would add a namespace to an org already at its cap is refused, naming the org and
the limit; editing or deleting the namespaces an org already has is never affected,
even at the cap.
- The default is 50. An org created before the field existed enforces at that same number.
- A personal org — the one minted for a solo self-serve signup — is stamped at 10 instead. Orgs onboarded before that keep whatever they have.
- There is no “unlimited” value: an org that genuinely needs more gets a larger explicit number.
- Only a Ringleader operator can change it. An
org-adminreads the field but cannot raise their own cap.
Org status
| Field | Type | Description |
|---|---|---|
message | string | The headline: the org's OIDC issuer documents are published, or the org's OIDC issuer documents are not published, so a cloud cannot validate an assertion for this org. |
oidc.published | bool | Whether the org’s issuer documents are live. |
oidc.keyIds | []string | The signing keys currently published for the org. |
oidc.lastPublished | string | When the documents last changed. |
oidc.message | string | The specific reason when publishing has not succeeded. |
This is what a CloudAccount in the org derives its readiness from: until the documents are published, your cloud has nothing to validate Ringleader’s assertion against, so no workstation governed by the org can mint a credential.
local is a reserved name: standalone mode seeds a degenerate local org
alongside the reserved local namespace, purely so org-aware code has one path to
run locally. It never leaves the device, and no one may create an org called
local.
Finding your organization id
The org’s id (not its name) is what a cloud trust configuration pins during
cloud onboarding. It is the
object’s uid, and it never changes:
rl org get acmecorp -o jsonpath='{.metadata.uid}'0192f5bf-af83-7178-8d0a-f1c7aea06bdeNames can be deleted and reused; a uid cannot. That is exactly why the cloud trust pins the uid.
Domain verification (OrgDomain)
An OrgDomain claims a DNS domain for an org. Verifying it is what enables self-serve membership.
apiVersion: core.ringleader.dev/v1
kind: OrgDomain
metadata:
name: acme-com
spec:
org: acmecorp
domain: acme.com| Field | Type | Description |
|---|---|---|
org | string | Required. The owning org. |
domain | string | Required. The DNS domain to verify, e.g. acme.com. |
Verifying it
Apply the object, then read the token it asks you to publish:
rl apply -f acme-com.yaml
rl orgdomain get acme-com -o jsonpath='{.status.expectedToken}'Publish it as a TXT record on the domain:
acme.com. IN TXT "ringleader-verification=<token>"A controller re-checks DNS periodically and flips the object’s Verified
condition:
rl orgdomain describe acme-com
rl od get| Status field | Description |
|---|---|
message | The one human headline, e.g. acme.com is verified or waiting for the ringleader-verification= DNS TXT record to appear on acme.com. |
conditions[] | Verified (True/False) with a reason — see below. |
expectedToken | The token to publish. |
lastCheckedAt | When DNS was last resolved. |
verifiedAt | When it first verified. |
consecutiveFailures | How many checks in a row failed to find the record. Drives the de-verification grace window. |
pendingChecks | How many checks have run while still waiting for the record. Drives the pending backoff. |
matchedKeyId | Which verification key the live record matched. Set only while verified. |
The Verified condition’s reason tells you which situation you are in:
| Reason | Meaning |
|---|---|
Pending | The token is minted; nobody has published the record yet. |
Verified | The record is live and matches. |
Rejected | Terminal: the domain is empty, reserved, or a public/free email domain. |
Conflict | Terminal: another organization verified this domain first. |
LookupFailed | De-verified after a grace window of failed DNS lookups. |
Four behaviours worth knowing:
- The token is deterministic, not random. The same domain and key always yield the same record, so you publish it once and a control-plane redeploy never invalidates it.
- All TXT records are scanned, not just the first, so the verification record can sit alongside every other TXT record on the domain.
- A single failed DNS lookup never de-verifies you. Public DNS fails
transiently, and revoking a whole org’s membership on one
SERVFAILwould be unacceptable, so de-verification takes several consecutive failures — visible inconsecutiveFailuresthe whole time.status.messagenarrates the grace window as it runs (acme.com is verified, but its DNS TXT record could not be confirmed on the last 2 checks; it will be de-verified after 3), which is your warning that a record went missing while the condition still readsTrue. - Free email domains are refused.
gmail.com,outlook.com,icloud.comand friends are rejected outright — otherwise one org could verify a public domain and absorb everyone who signs up with a matching address.
If two orgs claim the same verified domain, one deterministically wins and the
other is marked Conflict. Duplicate claims within one org never conflict.
How people join an org
On a user’s first login the control plane decides where they land, once. Three rules, in precedence order:
- An explicit pre-assignment by an operator wins over everything else. The
user is placed in a shared namespace as a
namespace-member. - A verified, auto-join domain. If the user’s email domain matches an
OrgDomainthat is verified and whose org hasautoJoin: true, they land in that org’s shared namespace (Org.spec.defaultNamespace, else a namespace named after the org) as anamespace-member. No operator involvement at all. - Otherwise, a personal namespace — derived from their email with a short
random suffix — which the user owns as
namespace-admin, rolling up to a personal org they are theorg-adminof.
Rule 3 makes a solo signup self-sufficient: because you administer your own personal
org, you can register a trust anchor and author a
service account to federate a CI job in,
with nobody having to act for you first. It applies to the personal branch only —
someone auto-joined into their company’s org under rule 2 lands as an ordinary
namespace-member, never as an administrator of the company.
Two gates guard rule 2, and both matter:
- The email must be verified by the identity provider. An unverified identity is never auto-joined into someone else’s org; it falls back to a personal namespace. It still onboards — it just never lands inside another tenant.
- The domain’s verification is re-checked at the moment of joining. A domain that lapsed between the lookup and the placement cannot still absorb the user.
Placement is decided once and not revisited. Someone who onboarded into a personal namespace before their company’s domain was verified stays where they are — auto-join only changes where new users land. Re-homing an existing member is a deliberate operator action, not something that happens behind their back; ask Ringleader if you need one moved.
Who administers an org
org-admin is a built-in role bound to a subject for one org by an
OrgRoleBinding. It is the root of its org and nothing more.
apiVersion: core.ringleader.dev/v1
kind: OrgRoleBinding
metadata:
name: acmecorp-admins
spec:
org: acmecorp
subjects: ["user:alice@acme.com"]
roleRef:
kind: GlobalRole
name: org-adminThat single object is the entire “make Alice an admin of Acme” step. It reaches every namespace of the org — including ones created later — so there is no per-namespace bootstrap to maintain.
An org-admin may:
- Create and manage the org’s namespaces (their
spec.orgmust be the admin’s own org, which is what makes the boundary structural rather than a convention). - Administer membership across every namespace of the org — bind and unbind
users to
namespace-memberornamespace-admin. - Author org-scoped roles (
OrgRole/OrgRoleBinding), bounded by the escalation guard: every rule they grant must be one they already hold. - Write the org’s CloudAccounts — which cloud identity to use for cloud federation.
- List the org’s members (
rl user get), folded to their own org. A normal member gets a403; another org’s users are invisible, not merely filtered.
An org-admin may not grant themselves more than they hold, write global roles, or reach any other org. They are deliberately not a wildcard.
Custom org roles
apiVersion: core.ringleader.dev/v1
kind: OrgRole
metadata:
name: ci-bot
spec:
org: acmecorp
rules:
- apiGroups: ["core.ringleader.dev"]
resources: ["workstations"]
verbs: ["get", "list", "watch"]
---
apiVersion: core.ringleader.dev/v1
kind: OrgRoleBinding
metadata:
name: acmecorp-ci
spec:
org: acmecorp
subjects: ["sa:acmecorp/ci"]
roleRef:
kind: OrgRole
name: ci-botBoth objects carry spec.org, and the role is only accepted because every rule in
it is a subset of what its author already holds.
Onboarding a company, end to end
Company Acme, domain acme.com, first admin alice@acme.com.
1. Ringleader creates the org, its landing namespace, and its domain claim.
apiVersion: core.ringleader.dev/v1
kind: Org
metadata: { name: acmecorp }
spec:
displayName: Acme Corporation
autoJoin: true
defaultNamespace: acmecorp
---
apiVersion: core.ringleader.dev/v1
kind: Namespace
metadata: { name: acmecorp }
spec: { org: acmecorp }
---
apiVersion: core.ringleader.dev/v1
kind: OrgDomain
metadata: { name: acme-com }
spec: { org: acmecorp, domain: acme.com }2. Acme publishes the TXT record. The domain verifies on the next check.
3. Ringleader appoints the first org-admin — the one OrgRoleBinding shown
above. Alice is now the root of Acme, and no further staff action is needed.
4. Alice creates the team namespaces.
apiVersion: core.ringleader.dev/v1
kind: Namespace
metadata: { name: acmecorp-team-a }
spec: { org: acmecorp } # must be Alice's own org, or it is denied5. Acme employees log in and land automatically. bob@acme.com signs in,
matches the verified domain, and is onboarded into acmecorp as a
namespace-member. Nobody does anything.
6. Alice places people into teams.
apiVersion: core.ringleader.dev/v1
kind: RoleBinding
metadata: { name: bob-team-a, namespace: acmecorp-team-a }
spec:
subjects: ["user:bob@acme.com"]
roleRef: { kind: GlobalRole, name: namespace-member }7. Acme connects its cloud. Follow cloud onboarding, which uses the org’s uid from step 1 as the subject its cloud trust pins.
Where you see your org
rl org get
rl auth status -o json # includes the org your default namespace rolls up toSee also
- Access control: namespaces, ownership, the role ladder, and Grants.
- CloudAccount: an org’s link to a cloud account, recording the cloud identity to use.
- Per-organization federation: how the org’s id becomes the subject your cloud trusts.