Skip to content

Roles and access

An operator’s permissions are set through roles, not on the operator directly. A role is a named set of permissions plus an optional scope selector. An operator gains permissions by being bound to roles. The whole RBAC model — roles, their permissions, and operator bindings — lives in PostgreSQL and is managed through the Operator API (REST/MCP/web UI), not by editing a config file.

A permission is an atomic allowed action. The format is one of two:

  • * — full access to all operations (equivalent to the cluster-admin role);
  • <resource>.<action> — exactly two segments: resource and action. Examples: incarnation.create, soul.list, role.grant-operator, operator.revoke.

The action can be a * wildcard within a resource — incarnation.* means “all actions on incarnations”. A wildcard of the form *.create is not supported; full access is the separate * case (with no dot).

A permission maps to a single Operator API operation: an HTTP endpoint and the identically named MCP tool. For example, incarnation.create governs the same action whether it’s called over HTTP, through MCP, or from the web UI.

The full list of available permission names is a backend catalog, not a table hard-coded into the docs. A name outside the catalog is rejected by Keeper when a role is loaded, with an unknown_permission error. The web UI and tools pull the current set from the API — so what’s given here is the mechanics, not a list.

Permission groups are organized by resource, for example:

  • operator management — operator.* (creation, revocation, token issuance, listing);
  • role management — role.* (creation, modification, deletion, binding/unbinding operators);
  • group management — synod.* (Synod);
  • working with incarnations — incarnation.* (creation, running, reading, history, updating);
  • the host registry — soul.* (registration, listing, assigning covens, token issuance);
  • recurring runs — cadence.*;
  • and other families.

The concrete set of actions and their semantics are what the backend catalog returns; check against it, not against a fixed list.

A permission can be narrowed with a selector — a filter that limits the action to a subset of souls. Without a selector, a permission applies without restriction.

A selector is written as on <key>=<values> after the permission. Supported keys:

KeyLimits the action to…
coven=…hosts/incarnations with the given Coven tags
service=…incarnations of the given service types
incarnation=…a specific incarnation by name
host=…a specific host (by its SID)
regex='…'…hosts whose SID/name matches an RE2 pattern
soulprint='…'…hosts whose facts satisfy a CEL predicate over soulprint.self.*

Multiple values for one key are listed comma-separated with no spaces and combine with OR: coven=db,cache — “coven db or cache”. Values for regex and soulprint are enclosed in single quotes so that commas and spaces inside the pattern/predicate don’t break the value apart.

An example of a role limited to an environment:

name: db-operator
permissions:
- "incarnation.* on service=redis-cluster,vault-cluster"
- "soul.list on coven=db"

Such an operator can perform any operation on incarnations of the redis-cluster and vault-cluster services and see hosts tagged db; everything else is forbidden by the default-deny policy.

A role can set a default scope once (the default_scope field) that all its permissions inherit. A per-permission selector (on … right in the permission string) overrides the default for that specific permission. This is handy when the whole role works in one environment but one or two actions need to be narrowed or widened individually.

How scope affects which hosts and incarnations an operator sees in listings — the Synod and Purview section.

An operator gains permissions when its AID is bound to a role (membership). Management is through the Operator API:

  • creating a role with a set of permissions — the role.create permission;
  • binding an operator to an existing role — the role.grant-operator permission (by the “role + AID” pair);
  • unbinding — the role.revoke-operator permission;
  • changing a role’s permission set — the role.update permission;
  • deleting a role — the role.delete permission;
  • listing roles with their permissions and bound operators — the role.list permission.

Binding is idempotent: binding the same AID to the same role again is a safe no-op.

Role management is subject to two safeguards (see Default-deny):

  • Least-privilege. You can’t include in a role, or grant through a role, a permission you don’t hold yourself. An attempt returns 403 forbidden. Full access (*) can be granted only by a holder of *.
  • Self-lockout. You can’t strip a role’s permissions or unbind an operator in a way that would leave the cluster without an active administrator with full access — 409 would-lock-out-cluster.

The built-in cluster-admin role (builtin, permission *) has extra protection: it can’t be deleted (role.delete) or have its permission set changed (role.update) — an attempt returns 409 role-builtin. Operators can be bound to and unbound from it (with the same self-lockout invariant on unbinding).

Permission application — instant across the cluster

Section titled “Permission application — instant across the cluster”

Keeper doesn’t hit the database on every permission check: it keeps a snapshot of roles and bindings in memory. Any mutation of a role, a permission set, or a binding broadcasts an invalidation signal across the cluster, and all instances re-read the snapshot from PostgreSQL. So a permission change takes effect on all instances almost immediately, not “eventually”. As a safety net against a lost signal, there’s a periodic background refresh of the snapshot.

This is separate from the lifetime of an already-issued JWT: revoking a role affects permission checks immediately, but a given operator token stays valid until its exp (Archons → Revocation).

  • Operator management — an overview of the Archon → roles → permissions model.
  • Synod and Purview — operator groups and scoped souls visibility.
  • Archons — the operator lifecycle and the last-administrator invariant.