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: Org

A 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.com org auto-joins the next Acme employee, instead of stranding them in a lonely personal namespace.
  • An administrator with reach. One org-admin grant 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
FieldTypeDescription
displayNamestringHuman label.
planstringfree, team, or enterprise.
suspendedboolHard stop (non-payment, abuse). Consumers gate on it.
autoJoinboolWhether someone with an email at a verified domain of this org joins it automatically.
defaultNamespacestringThe shared namespace new members land in. Unset: a namespace named after the org.
billingAccountRefstringA reference into the external billing system of record — never a ledger.
maxNamespacesintegerHow many namespaces this org may hold. Minimum 1; defaults to 50.
settingsobjectFree-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-admin reads the field but cannot raise their own cap.

Org status

FieldTypeDescription
messagestringThe 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.publishedboolWhether the org’s issuer documents are live.
oidc.keyIds[]stringThe signing keys currently published for the org.
oidc.lastPublishedstringWhen the documents last changed.
oidc.messagestringThe 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-f1c7aea06bde

Names 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
FieldTypeDescription
orgstringRequired. The owning org.
domainstringRequired. 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 fieldDescription
messageThe 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.
expectedTokenThe token to publish.
lastCheckedAtWhen DNS was last resolved.
verifiedAtWhen it first verified.
consecutiveFailuresHow many checks in a row failed to find the record. Drives the de-verification grace window.
pendingChecksHow many checks have run while still waiting for the record. Drives the pending backoff.
matchedKeyIdWhich verification key the live record matched. Set only while verified.

The Verified condition’s reason tells you which situation you are in:

ReasonMeaning
PendingThe token is minted; nobody has published the record yet.
VerifiedThe record is live and matches.
RejectedTerminal: the domain is empty, reserved, or a public/free email domain.
ConflictTerminal: another organization verified this domain first.
LookupFailedDe-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 SERVFAIL would be unacceptable, so de-verification takes several consecutive failures — visible in consecutiveFailures the whole time. status.message narrates 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 reads True.
  • Free email domains are refused. gmail.com, outlook.com, icloud.com and 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:

  1. An explicit pre-assignment by an operator wins over everything else. The user is placed in a shared namespace as a namespace-member.
  2. A verified, auto-join domain. If the user’s email domain matches an OrgDomain that is verified and whose org has autoJoin: true, they land in that org’s shared namespace (Org.spec.defaultNamespace, else a namespace named after the org) as a namespace-member. No operator involvement at all.
  3. 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 the org-admin of.

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-admin

That 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.org must 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-member or namespace-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 a 403; 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-bot

Both 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 denied

5. 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 to

See also