RBAC roles + capabilities
Kirimana’s access model is deliberately small: four roles, a handful
of capabilities, and one rule for scoping them to an environment and a
domain. The whole thing is machine-readable in one place, so the CLI,
the web app, and the MCP identity passthrough all decide access the
same way. Identity comes from your OIDC provider; the mapping from
identity to role lives in kiri_rbac.yml.
Four roles
The roles are additive going left to right — each strictly contains the rights of the one before it:
| Role | Value | What it can do |
|---|---|---|
| Viewer | viewer | Read-only, in every env. |
| Author | author | Apply contract changes in dev/test (scoped by domain). Loses apply in prod. |
| Approver | approver | Everything an author can, plus approve/promote/release in every env. |
| Platform-admin | platform-admin | Everything, including editing platform config and redacting audit logs. |
When a caller’s identity groups map them to more than one role in the same env × domain cell, the strictest applicable role wins — the resolver picks the one with the most rights, not the least, so a narrowly-scoped elevated binding is honoured rather than diluted.
The capability matrix
Roles are checked against capabilities, not against individual commands. The capabilities are kept small on purpose — if a new operation cannot be classified as one of these, the answer is usually to add a domain, not a capability.
| Capability | Who | Notes |
|---|---|---|
read | viewer+ | Read-only access, every env. |
apply | author+ in dev/test; approver+ in prod | Author loses apply in prod — production cuts are an intentional, elevated act. |
release_apply | approver+ on test/prod; platform-admin everywhere | The production-cut operation (kiri release apply). |
approve | approver+ | PR approval / contract promotion gate. |
promote_gold | approver+ in the owning domain | Medallion promotion to gold. |
edit_platform_config | platform-admin only | Edit kiri.yml auth/domains, kiri_rbac.yml, vault or adapter config. |
redact_audit | platform-admin only | Redact an audit-log entry. Separate cohort from config edits — different blast radius, different PR gate. |
dispatch_incident | viewer+ in dev/test; author+ in prod | Manually fire an ITSM ticket; the bar lifts in prod because a real on-call gets paged. |
The one rule worth memorising: author loses apply in prod. In dev and test an author lands their own changes; the moment the target is prod, applying becomes an approver-or-above act. That single tightening is what makes a production cut a deliberate handoff rather than something an author can do alone.
Scoping: env × domain
A role is never granted globally by accident. Bindings in
kiri_rbac.yml map an identity group to a role, optionally narrowed
to specific environments and/or domains:
version: 1
auth:
provider: github
group_claim: teams
role_bindings:
- group: acme/platform-team
role: platform-admin # every env, every domain
- group: acme/sales-eng
role: author
domains: [sales] # sales domain only
- group: acme/sales-leads
role: approver
environments: [prod]
domains: [sales] # approver, but only sales in prod
Omitting environments means “every env, including ones added later”;
omitting domains means “every domain”. An empty list is rejected
at load time as ambiguous — use omission or null to mean “all”. Two
bindings with an identical (group, role, environments, domains)
tuple are also rejected, as a copy-paste smell. The resolver picks the
strictest role for the exact env × domain cell an operation touches,
then the capability matrix decides the rest.
OIDC-delegated identity
Kirimana never stores its own user database. Caller identity is
delegated to your OIDC provider, named by the auth.provider
discriminator (github, azure_ad, oidc_generic are supported;
okta and google are declared but provider plugins are deferred).
The group_claim names the token claim that carries group
membership — GitHub emits teams, Azure AD emits groups — and those
group values are exactly what your role_bindings match on. The
auth: block here is kept in sync with the one in kiri.yml; the
loader validates the two do not silently desync.
The dev bypass, and why prod ignores it
For local development you can act as a role without an OIDC login,
using --as-role (or the KIRIMANA_AS_ROLE env var):
kiri apply --target dev --as-role approver
This is honoured only when two conditions both hold:
dev_bypass: true is set on the kiri_rbac.yml auth block, and
the target env is not prod. A --as-role override against prod is
rejected unconditionally — the safety rail is fixed in production,
which always requires real identity. Every bypass invocation is
audit-logged with acting_as_override=true, so a forensic answer to
“who was actually at the keyboard” always exists.
What a denial looks like
Privileged CLI commands check access near the top and, on denial, print a clear reason and exit with code 3 — a code reserved for RBAC denials specifically, so a shell script can tell a permission problem from a bug. The message names the missing right; the author-in-prod case even points you at the fix:
✗ Access denied. role=author cannot perform apply in env=prod.
Author loses apply rights in prod. Ask an approver to run
`kiri release apply`, or open a PR.
If no RBAC is configured at all, Kirimana degrades to a single-user mode where everything is allowed — but the audit trace still records the OS-level subject, so even an unconfigured project leaves a trail.
How this composes with the rest of governance
RBAC answers “may this identity act here?”; the surrounding machinery answers the other governance questions. Domain owners + CODEOWNERS routes contract PRs to the right reviewers. PR-time governance gates enforces classification, ownership, and lineage at merge. And the capabilities above are exactly the gates the contract-approval review leans on when a change needs a human decision recorded.