A principal may have several roles. Agents are scoped to their own runs; every other role sees all runs.
API keys
keys.json
A key needs
key or key_sha256 and a valid role, or the whole file is rejected.
Hash keys (printf '%s' "$KEY" | shasum -a 256) rather than storing them. Generate keys with at least 128 bits of entropy
(openssl rand -hex 24). Keys are compared in constant time across all entries.
Rotation without downtime: add the new key under the same name with a not_before, give the old one an expires_at,
hand out the new key, and remove the old entry later. The server reloads the file on SIGHUP or when it changes (checked
every two seconds); a reload that fails to parse keeps the old keys. disabled: true switches a key off immediately.
Managing keys from the server
When the server runs withREVERSI_KEYS, an admin can manage the file over HTTP or in the console instead of editing it.
The server rewrites the file (mode 0600) and applies the change at once.
All four need the
admin role. In the console, Settings has a Users and API keys table with create (name, role,
groups, tenant, days to expiry), disable, enable and remove; a new key is shown once, in the response.
OIDC
Bearer JWTs from your identity provider:none, never HMAC), a kid in
the header, iss, aud containing your audience, exp and nbf (60 s skew). The audience check is what stops a token
minted for another service from working here: it is required.
- JWKS. Found through the issuer’s discovery document (
/.well-known/openid-configuration) and cached. The key set is re-read every 10 minutes even for known keys, so a key the issuer withdrew stops verifying, and when an unknownkidappears, at most once every 10 seconds so a flood of bad tokens cannot hammer the issuer. - Roles. The role claim may be a string, a list, or a nested path like
realm_access.roles. Each value is looked up in the role map; unmapped values are ignored, and a token with no mapped role is rejected. - Groups. Every value of the role claim also counts as a group name, so an approval can be routed to an identity
provider group (
approvers: [finance-approvers]) with no extra mapping. - Name.
sub, or the claim named byREVERSI_OIDC_ACTOR_CLAIM. - Tenant. The verifier can read a tenant from a claim (
tenant_claimin the library), but the server has no setting for it yet, so OIDC principals carry no tenant there.
Console modes
The console is controlled byREVERSI_CONSOLE (or console.mode in reversi.config.yaml).
open refuses to start unless HOST is loopback (127.0.0.1, ::1 or localhost), and refuses admin as its role. While
running it answers only requests whose Host is a loopback name and whose Origin, if any, is the same host; anything else
gets 403. So key management, break-glass, holds and the kill switch are not available in open: use login for those.
OIDC browser redirect login (a “Sign in with SSO” button) is not built. In login mode a person pastes an OIDC access
token; there is no redirect flow.
Sessions
A login produces a signed cookie (HMAC-SHA-256,HttpOnly, SameSite=Strict, eight hours). For API keys the cookie holds the
key’s fingerprint, and every request looks the fingerprint up in the live key ring. Disabling, expiring or removing the
key ends the session at once, and changing its roles or groups changes what the session may do on the next click. An OIDC
session never outlives its token. Logging out invalidates the cookie. Set REVERSI_SESSION_SECRET so sessions survive a
restart, and REVERSI_SECURE_COOKIES=1 behind HTTPS.
Slack approvals: user mapping
Slack buttons approve as a Reversi person. The Slack user id is looked up in a map you provide.reversi-server turns the
callback (POST /integrations/slack/actions) on when both REVERSI_SLACK_SIGNING_SECRET and REVERSI_SLACK_USERS are set;
REVERSI_SLACK_USERS is the path of a JSON file:
404 Slack approvals are not enabled; a secret without the map file stops the server at
start. In code the same map is createApp({ slack: { signing_secret, users } }).
roles defaults to ["approver"]. A mapped user whose roles do not include approver or higher (an auditor, say) cannot
decide: they get the same “not set up as an approver” answer as an unmapped user. The request must carry a valid Slack
signature and a timestamp within five minutes (otherwise 401). Checked locally with signed requests: an unmapped user and a
mapped auditor are refused, a mapped approver decides the request. Not tested against real Slack. See
approvals.
