Governance & access control
Governance turns the control plane from a single-token service into a multi-user, role-based system with SSO, service tokens, and a tamper-evident audit log. It is default-off: with governance off, the control plane keeps the single static bearer token and none of the endpoints below exist.
Everything here runs in the control plane — it is zero-datapath and never touches the WAF.
Enable it
[controlplane.governance]
enabled = true
session_ttl_hours = 12
cookie_secure = true # set false only for local http dev
bootstrap_admin = "admin" # created if there are zero users at boot
Two environment secrets are required:
| Env var | What it is |
|---|---|
WAF_CP_AUDIT_KEY |
64 hex chars (an Ed25519 seed) that signs the audit chain. Must persist across restarts or the chain becomes unverifiable under a new key. |
WAF_CP_BOOTSTRAP_ADMIN_PASSWORD |
the initial password for bootstrap_admin, used once when there are no users. |
On first boot with no users, the bootstrap admin is created; log in and create the rest. When
governance is on, /api/* is gated by either a session cookie (a human, with RBAC per role) or the
bearer token (a machine/admin principal).
Roles (RBAC)
Three built-in roles, each a superset of the previous:
| Role | Can do |
|---|---|
viewer |
read observability — Overview, Events, Alerts, and the read API |
operator |
everything a viewer can, plus manage rules (publish/rollback) and view captured requests (ViewCaptures) |
admin |
everything, plus user & token management, sessions, the audit log, and compliance reports |
Viewing a captured request (the full denied request — see request capture)
needs the ViewCaptures permission (operator and up), because captures may hold raw secrets; each view is
recorded in the signed audit log.
The dashboard hides views a role can’t use, but the gating is enforced by the backend — a viewer
who calls an admin endpoint gets 403 regardless of the UI.
Users
Admins manage local accounts from the Users view (or GET/POST /api/users,
PATCH/DELETE /api/users/{id}):
- create a user with a username, password, and role;
- change a user’s role inline;
- disable/enable an account (disabling revokes its sessions immediately);
- reset a local user’s password (revokes their sessions);
- delete an account.
SSO-provisioned users show a source of oidc and manage their password at the identity provider, not
here.
Service tokens
Named, revocable, role-bound tokens for machines (the node agent, CI, scripts) — created under
Tokens (GET/POST /api/tokens, DELETE /api/tokens/{id}, admin). A token’s role bounds what it
can do, exactly like a user’s. The plaintext is shown once at creation — copy it then; it can’t be
retrieved later, only revoked.
Sessions & passwords
- Your sessions — the Account view lists your active sessions and lets you revoke any but the
current one (
GET /api/sessions,DELETE /api/sessions/{id}). - Change your password — from Account; this revokes your other sessions
(
POST /api/me/password). - Admin reset — an admin can reset any local user’s password (
POST /api/users/{id}/password) and revoke all of a user’s sessions (DELETE /api/users/{id}/sessions).
Login throttle
Brute-force protection locks out a username after too many failed logins in a window:
[controlplane.governance]
login_max_failures = 5
login_window_seconds = 60
login_lockout_seconds = 300
SSO via OpenID Connect
Modern SSO uses OIDC (Authorization Code + PKCE). The IdP is discovered from issuer_url; login goes
through the browser and ends in a session cookie. A role is mapped from a claim (typically IdP groups);
a user matching no mapping is denied unless default_role is set.
[controlplane.governance.oidc]
issuer_url = "https://idp.example.com/realms/waf"
client_id = "waf-cp"
redirect_url = "https://cp.example.com/api/auth/oidc/callback"
role_claim = "groups"
default_role = "viewer" # optional; omit to DENY users matching no role_map entry
[[controlplane.governance.oidc.role_map]]
claim = "waf-admins"
role = "admin"
The client secret comes from WAF_CP_OIDC_CLIENT_SECRET. When OIDC is configured, the login screen
shows a Sign in with SSO button in addition to username/password. SSO users are provisioned
locally with auth_source = oidc and never collide with local accounts.
SAML. Native SAML is intentionally not supported. Federate a SAML IdP behind an OIDC broker (Keycloak/Dex) and point the control plane at the broker — see the runbook and BOUNDARY for the rationale.
The audit log
Every governance action (logins, user/token/session changes, password changes, rule publishes and rollbacks) is written to a signed, hash-chained audit log. Each record links to the previous by hash and is signed with the audit key, so any insertion, deletion, or edit breaks the chain.
- View — the Audit view (admin) shows records newest-first (
GET /api/audit). - Verify — Verify chain recomputes the hash chain and checks every signature
(
GET /api/audit/verify), reporting the number of records verified and the signing public key, or the exact sequence where the chain broke.
Because the chain is keyed by WAF_CP_AUDIT_KEY, keep that seed stable across restarts and back it up —
losing it means past records can no longer be verified under the same public key.