Administration console — design
Scope: what the administration console is, what each of its screens does and which service answers it.
Companion docs: architecture.md, iam.md, security/design.md, multi-tenancy.md, routing.md, store-contract.md, resource-plans.md.
1. Purpose
Every tenant needs a place to govern who belongs to it and which apps
each person may use. The administration console is that place. It is an
app: a component with its own profile (admin-console, shipped by the
operator chart with placement: {tenants: every}), so every tenant has one at
admin.<tenant domain>, behind the tenant's session. It reaches a person as a
tile on the desktop, shown to whoever holds can_administer on the tenant and
to nobody else. The platform tenant has one too, at admin.platform.<KERNEL_DOMAIN>.
A console administers the tenant it is installed in and no other. A platform administrator reaches another tenant through that tenant's own console.
The code is in gentian-ui,
apps/admin-console: a React frontend and a small backend. The console
holds no credential, keeps no state and decides nothing. Its backend checks
the token the edge forwards and relays each call, with that token, to one of
four services in kernel-control; what a person may see or change is that
service's answer from OpenFGA.
| Service | Asked for |
|---|---|
| director | Everything that is declared state: a write is a commit to the deployments repository, authored as the person. Also actions that happen once (take an export, publish a notice, purge an app's data) |
| usher | Reads of what the cluster holds: installed components and their state, resources, exports, notices |
| registrar | People and groups. It holds one Keycloak credential per realm; the console holds none |
| custodian | Credentials a person sets: repository credentials, the backup identity |
1.1 Screens
| Screen | What it does | Answered by |
|---|---|---|
| Tenants (platform administrator) | Create, retire and purge tenants, issue a tenant administrator's activation link | director, registrar |
| Members | List, invite, update, switch off, remove; password reset; require or remove an authenticator; mailboxes of removed people (§4.3a) | registrar |
| Groups | Custom groups and who is in each group, the app groups included | registrar |
| Apps | The tenant's installed apps. Per app: state, who has access and whether it is for everyone, integrations, what it asked of the platform and what was approved, public addresses and their approval, uninstall, purge. Uninstall keeps the app's data; purge destroys the data of an app that is no longer installed (store-contract.md §8) | director, usher |
| Resources | Plan, ceiling, usage history (§4.8) | usher (read), director (write) |
| Export | Take one export now; download or delete the exports a person started. Scheduled backups are not listed | usher (read), director |
| Security | The realm's password, session and lockout policy (§4.5) | director |
| Integrations | What the tenant's apps consume from each other, and the grants | director |
| Credentials | Repository credentials, backup identity | custodian, director |
| Notifications | Publish a notice to the tenant's people (§5) | director, usher (read) |
| Audit | Changes to declared state, from git (§4.7) | director |
| Cluster settings, Platform security, Customization, Usage Statistics (platform administrator) | The Cluster claim's settings, permitted waivers, customization debt, the last usage statistics report | director |
| Role holders (platform administrator) | Who holds cluster#security_officer or cluster#auditor by name: a picker over every tenant's administrators and a role, the holders as the Cluster claim declares them (§7) | registrar (the picker), director |
| Approvals (security officer; the console of the officer's own tenant) | Every tenant's app requests of a privilege that weakens what protects the node — pod-security waivers, cluster roles — with the profile's reason and who asked for it; Approve with a reason. The list is the usher's (GET /v1/clusters/{c}/approvals, can_approve or can_configure); the approval is the director's privilege route, decided by the kind (store-contract.md §8) | usher (read), director |
| Models (platform administrator) | The models the model gateway offers, which are the Cluster claim's: providers and their models, the models the cluster serves itself, the two serving switches, and the switch for the gateway's own console. Each model is flagged when it cannot answer. No token is entered here (§6a) | director; custodian (read, for the token's state) |
| Catalogs | Hidden. The cluster renders no catalog of its own; the screen is kept and not linked |
Nothing is installed from the console. Apps come from the App Store app, a
component of its own beside the console (store-contract.md
§6), or from kubectl gentian apps install where there is no store.
1.2 Three interfaces, one rule
The console is one of three interfaces the platform serves on a cluster, beside the desktop and the App Store app, and one rule holds for all of them: a UI carries no authority. It renders what an API returns and turns clicks and key presses into API calls made with the signed-in person's token. It holds no ServiceAccount with write verbs, no admin credential, no master key, and decides nothing — every "may this person see or do X" is a verdict it received from a named enforcement point (AD-1, security/principles.md §3), never a branch it evaluated.
A single-page app cannot keep a confidential client secret or a refresh token
safely, so something server-side has to hold the session. That something is
the edge: the Gateway runs the code flow for the whole tenant zone and
forwards the token on the routes whose entry asks for it (forwardToken,
routing.md). A backend-for-frontend therefore exists only for
same-origin relaying and for the small amount of UI state an interface
keeps — for the desktop, per-viewer preferences and the notification inbox,
in the tenant's own database, granted as a requirement of its profile. It
runs no code flow, holds no OIDC client secret and has no Kubernetes RBAC: it
relays an identity the edge established.
| Interface | Where | For whom | Shows | Does |
|---|---|---|---|---|
| Desktop | on the cluster, per tenant | everyone | the tiles the signed-in person may open: the usher's answer (GET /v1/tenants/{t}/tiles), filtered for the caller before it arrives | opens them. Nothing else: no install, uninstall, add-on, access, repository or store logic |
| Administration console (this document) | on the cluster, per tenant | administrators | people and groups, policies, resources, credentials, and under Apps every installed app: its state, who has access and whether it is for everyone, its integrations, its privileges | administers: gives and takes away access, sets "for everyone", approves privileges, uninstalls, purges. It installs nothing and lists no catalog |
| App Store app (store-contract.md §6) | on the cluster, per tenant; absent where the cluster offers no store | people who may install apps in the tenant | the store's data, at once and with no store account: apps, descriptions, pictures, reviews, evaluations and reports, versions, list prices. Signed in at the store: what the tenant has acquired | acquires from the store, which is what the sign-in to the store is for; installs through the director and sets the repository credential through the custodian |
| The store (store-contract.md §1) | outside the cluster, run by a vendor | — | nothing on the cluster. It serves data to the App Store app: the catalog to anyone who asks, with no token; a tenant's standing and acquisitions to a person signed in to it | data and commerce only. It never calls the cluster |
| Operations Console | on the cluster, where installed | whoever looks after the cluster | an add-on's own screens (data-ownership.md §5) | what the add-on does; it is not part of the OS |
| Command line | the administrator's machine | administrators | — | kubectl gentian apps install … through the director. With no store it is the only way to install an app |
The three interfaces on the cluster are separate components — the desktop
and apps/admin-console in the gentian-ui repository, and the App Store
app, which the store's vendor publishes — and none of them is a mode of
another.
What comes from where. The App Store app and the console never show the same fact from two places:
| From the store, shown by the App Store app | From the cluster, shown by the console |
|---|---|
| What an app is: name, summary, description, pictures | What is installed, at which digest |
| Reviews and their summary | Its state: installing, ready, failing, and why |
| Evaluations and reports | Who has access, and the "for everyone" setting |
| Editions, versions and the digest of each | Its integrations |
| Prices | Its privileges: what it requests of the platform and what was approved |
| What the tenant has acquired | What its data is, when it is uninstalled and when it is purged |
The App Store app needs one fact from the cluster to be useful — which of the store's entries this tenant has installed — and reads it from the director like any caller. It sends the store none of it.
2. Placement in the security model
Tenant isolation rests on two independent layers (see security/design.md):
| Layer | Mechanism |
|---|---|
| MAC backbone | tenant-<name> namespaces, default-deny NetworkPolicy, Kyverno |
| Identity domain | One Keycloak realm per tenant |
Keycloak Organizations (one realm, logical tenants) are not used: they share one user database. See iam.md §1.
Authority in the console is not read from a token's groups. Which screens a person gets is decided from two answers of the director: the verbs they hold on the tenant and on the cluster. Every relayed call is checked again by the service that receives it.
The desktop is a relay for tiles. It is the app of people who hold no
privilege. Code that installs, removes or grants, shipped to every member's
browser and reachable on every member's origin, is code a member can try to
drive; the checks behind it would hold, but the cheapest way to be sure a
path is not misused is for it not to be there. So the desktop carries no
install, uninstall, purge, add-on, access-granting, repository or store
logic — no route in its backend, no screen in its bundle — and the
administrative paths live in the two interfaces only administrators are
served: this console and the App Store app, which an administrator's tiles
lead to. The desktop does not know what an administrator is: the usher's
answer comes back already filtered for the caller, and can_launch derives
from the app's own entitlement group, not from tenant membership. Its AI
widget calls the model gateway with the desktop component's own granted
credential, an llm requirement of its profile, never a master key.
3. Identity topology
Realms, sign-in, the group names and the member/administrator split are described once, in iam.md §1.1–§1.3. What the console relies on:
- People and groups of a tenant are in the tenant's own realm; the platform
tenant's are in the
kernelrealm. - The login name is the email address.
gentian.inviteEmailis an optional second address used for the invitation, password reset and recovery. - Administrator and member are separate accounts:
can_useon an app excludes a tenant's administrators, so an administrator's desktop shows administration tiles and a member's shows apps.
4. What the screens do
4.1 One console, scoped by what the person holds
The same image runs for every tenant. The screens marked platform administrator in §1.1 are shown only to a person who holds the cluster's verbs; they are reachable from any console such a person can open.
The console's context (/admin/context) is refused to anyone who does not
hold can_administer on the tenant, with one widening: a person who holds
cluster#can_approve — the security officer — is let in with
isTenantAdmin: false and sees the Approvals screen alone, nothing of
the tenant's. A security officer who holds the role by name holds it signed
in to their own tenant (iam.md §1.1c) and acts in that tenant's
console; the App Store's install step links them there
(?tab=approvals). The tenant console keeps its per-app Approve on the Apps screen for the egress
requests, which are the tenant's perimeter approver's (can_approve_privilege).
4.2 First administrators
-
Platform administrator. The installer creates
admin@<KERNEL_DOMAIN>in the kernel realm with no password and issues a single-use activation link (./install.sh --activate-adminfor a new one). -
Tenant administrator. Provisioning creates the realm, its groups and the administrator account (
status.adminEmailon the Tenant), a member ofgentian:tenant:<t>:admins, with no password. The registrar issues the activation link:kubectl gentian tenants activate-admin <tenant>, or the Tenants screen.Tenants → Create asks for the administrator's recovery address. With an address, the console hands it to the registrar as soon as the tenant is committed, through its backend and with the person's token, and says that the invitation will be mailed to that address when the tenant is ready and that the page can be closed. The console keeps nothing: the registrar keeps the request and sends the invitation (iam.md §1.4). While the page stays open it shows the request's state. In the list, a tenant with a pending request shows Invitation pending in place of Activate administrator; when the invitation expired or could not be mailed, the button is back. Without an address nothing changes: the console waits for the tenant and shows the link once. If the registrar cannot keep the request, the console says so and waits with the page open.
No password is derived, stored or printed for either (iam.md §1.4).
Inviting a member. Members → Invite: address, name, optional recovery
address, the app groups to join (an app installed for everyone is preselected).
The registrar creates the account with no password and has Keycloak mail a
link with VERIFY_EMAIL and UPDATE_PASSWORD.
4.3 Password reset and recovery
| Action | Mechanism |
|---|---|
| Reset by an administrator | The registrar has Keycloak mail a link with UPDATE_PASSWORD, to gentian.inviteEmail if set, else the primary address |
4.3a Removing a member
Members → open the member → Remove member. The account is deleted.
Where the member has a mailbox on the cluster's own mail server, a dialog asks what becomes of it, with nothing selected:
- Archive the mailbox. The mail is kept, out of the live mailboxes. Nobody receives or signs in at the address; a new person given the address starts empty.
- Delete the mailbox. The mail is destroyed. It cannot be undone, and is confirmed a second time.
The button stays off until one is chosen. Switching a member off (May sign in) asks nothing and keeps the mailbox.
Mailboxes of removed people, on the same screen, lists what was decided
for each and what became of it: being archived or deleted, archived (with
who chose, when, and its size), deleted, or failed with the reason. An
archived mailbox is deleted from that list, after a confirmation. Who may do
all of this: whoever may manage the tenant's people (can_manage_users).
mail.md §5c says what happens on the mail server.
4.4 Second factor
Keycloak's built-in TOTP. An administrator requires an authenticator for one person (at the next sign-in, or by a mailed link) and removes a person's authenticators for a lost device. A realm-wide rule ("everybody must have one") cannot be set from the console: Keycloak expresses it as an authentication flow, not a realm setting. WebAuthn and passkeys are not configured.
4.5 Security policies
| Policy area | Settings |
|---|---|
| Password | Minimum length, digits, lower and upper case, special characters, history, maximum age |
| Session | Idle time, maximum length, remember-me |
| Lockout | On or off, failures allowed, lockout duration |
The policy is declared state: the director commits it as
security-policy.yaml beside the tenant's manifest (can_set_policy), and the
tenant Composition writes it into the realm. Nothing in this path holds a
Keycloak credential, and a realm rebuilt from scratch comes back with its policy.
This is the one way to set it. The console relays the signed-in person's call to the director and holds no right of its own; the registrar has no action for the password policy and its Keycloak client holds no role over realm settings. The Composition always declares the realm's password policy: a tenant that states no password rules has an empty one, which is Keycloak's own default, so rules taken out of git are taken out of the realm, and a policy set by hand at Keycloak is replaced by what git says.
4.6 Sessions
The console has no Sessions screen.
What ends a session when an administrator switches a member off or removes them is the realm's refusal to renew it, within five minutes; nobody ends it by hand. The whole of it is in iam.md §1.12.
4.7 Audit
The screen shows changes to declared state: every such change is a commit the director authored as the person, with the relation and object that permitted it. It needs no store of its own.
Not in it: sign-ins, refused requests and reads of data, for which no store exists, and changes to people. For those the registrar keeps a record of who asked and what permitted it, with a retention period, beside Keycloak's own admin events; the console does not show either. The screen says what it leaves out. There is no export.
4.8 Resources
Maps to the OS question "how much of this machine may this account use, and how much is it using?" — with the part a desktop OS has no answer for: what that came to over a month.
Full design in resource-plans.md; what matters to the console is the shape.
| Capability | Tenant admin | Platform admin |
|---|---|---|
| Current ceiling | Own tenant: enforced limits paired with committed use, and live consumption where a metrics source exists | Any tenant, plus an all-tenants headroom table |
| Plan catalog | The cluster's plans, each one the tenant could not move to carrying its reason | Same |
| Another plan | The cluster's notice on where to order one; no choice | Changed in the Operations Console or with kubectl gentian resources set |
| Usage history | Own tenant, per resource, over 7d–12m | Any tenant |
| Billed intervals | The stretches the window resolves to, with the SKU in effect over each | Same |
Three properties make this different from an editable quota field:
The API takes a plan name, never a quantity. A ceiling that can be any number can be any number that was never sold, so every ceiling reachable through the console is one the platform has priced — which is what makes a month resolve to SKUs rather than to numbers somebody downstream has to interpret.
The write is a commit, not a patch, and it is the cluster's. Moving a
tenant is the director committing resource-plan.yaml to the deployments
repository as the cluster's administrator who asked, after checking
can_configure on the cluster and validating the choice against the
operator's answer for that tenant. A tenant orders another plan where the
cluster's notice says. The reads are the operator's, relayed by the usher;
kubectl gentian resources reads them the same way and writes through the
director (set).
A downgrade below current use is refused. Kubernetes does not evict pods to fit a shrunken quota — it refuses the next create — so shrinking a tenant too far fails silently, hours later, at the next restart. The console names the resource and both numbers instead.
A plan change is audited as the commit it is, authored by the person and trailered with the decision that allowed it, and as the plan event the operator records once the change has landed; a refusal is the director's decision-log entry. The attempt is the interesting half when a tenant later asks why nothing changed.
5. Notifications
An administrator publishes a notice (a title and a text) to the people of the
tenant. Publishing is an action at the director (can_administer), which
records who published it. Notices are stored in the tenant's own database,
in the table the desktop reads; the console keeps no copy and reads the
history through the usher.
6. App access
Who may use an installed app is membership of the app's Keycloak group,
gentian:tenant:<t>:app:<profile>, and nothing else. The console sets it: per
person on Members, per group on Groups, and on Apps whether the app is
for everyone. How membership becomes a decision at the front door and a tile
on the desktop is iam.md §1.6.
Accounts inside apps. The platform does not create or remove a person's
account in an app; an app makes one at the person's first sign-in. The one
thing synchronized is who administers an app: members of
gentian:tenant:<t>:app-admins are given the administrator role an app's
profile declares (spec.hooks.provisioning, iam.md §1.8).
6a. Models
The model gateway serves the models the Cluster claim declares and no others
(llms.md §5). The Models tab is where the cluster's
administrator reads and changes them: GET and PUT /v1/clusters/{c}/models
of the director, under can_audit and can_configure on the cluster. A
tenant's administrator has no such tab and is refused by the director.
A save is one commit of the claim. What is not on the screen when it is committed is removed from the gateway. The gateway restarts with the new list once Argo CD has synced.
The tab flags a model that cannot answer, without asking the gateway: a model the cluster would serve itself is "not served", because the platform starts no server for it; a provider's model is "token missing" or "no credential" when the custodian's list says so. A token that is there is shown as supplied, not as working.
A provider's token is entered on the Credentials tab, under
llm-provider-<name>. The cluster declares that credential for every provider
on the claim, so a new provider's appears there once the cluster has applied
the commit. Which property of it holds the token follows from the provider's
name; the tab shows it and does not ask for it. The director refuses a provider
address that is not a public https address.
The tab also switches the gateway's own console on and off
(llm.console.enabled), with a warning that it opens a public address for the
gateway's console and API. The route and the gateway's network rule both
follow that commit.
7. Platform roles
Which Keycloak group holds admin and breakGlass is stated on the Cluster
claim (spec.platformRoles; admin defaults to gentian:platform:admin,
breakGlass has no holder unless a group is named). The operator projects it
into OpenFGA. Who is in those groups is not changed from this console by
hand: the registrar refuses every write to a platform role's group and to its
members. Every other cluster role -- security officer, auditor, service
admin, shared-apps admin, perimeter approver -- is held by name: the Role
holders page lists the candidates (every person of the platform tenant and
every other tenant's administrators), the cluster's administrator picks one,
a role and a reason, optionally an expiry, and the director commits the
choice to the Cluster claim (spec.platformAccess.roleHolders); the operator
writes the role for that account of that tenant's realm, over the cluster or,
for a tenant's perimeter approver, over that tenant (iam.md §1.1c,
commands.md §7). A platform
administrator administers a tenant the cluster operates through that tenant's
console, and signs in to it only where the tenant's administrators switched
that on (Security → Platform administrators may enter this tenant,
Tenant.spec.platformAccess.adminsEnter); the Tenants page then offers
Enter tenant.
8. Non-goals
These stay outside the console:
- Installing apps (the App Store app, or the command line)
- Scheduled backups, the backup policy and its destinations (the Operations Console)
- Editing NetworkPolicy, Kyverno policy or other MAC rules
- Authoring a
ComponentProfileor a catalog
9. References
| Topic | Location |
|---|---|
| Roles and sign-in | iam.md |
| Hosts, the front door, sessions | routing.md |
| MAC backbone | security/design.md |
| OpenFGA model | authz/model/v1/model.fga |
| Tenant identity objects | iam.md §1.9 |
| The App Store app | store-contract.md |
| Resource plans and usage | resource-plans.md |