RBAC Security ModelEN
RBAC Security Model (Cert-First)
Version: 2026-08-28 (aligned with commits
cc46b20/117e35b/f7355b2) Applies to: any deployment withrbac.enabled=true
Authoritative security model for the varwof-core permission system. It answers the three audit-critical questions: whose authorization is trustworthy, what credentials reach what role, and where the boundary is.
1. Trusted authority: certificates only, accounts never
Authorization (role + permission vector) comes only from claims embedded in the mTLS management certificate:
- Role ← certificate OrganizationalUnit (OU) → maps to
superadmin / admin / operator / auditor / readonly / revoker / auto-renew / reporter, etc. - Permission vector ← certificate PrincipalAuthorization (PA) extension (certificate-only;
user.HasPermis decided on PA). - CA scope ← SAN URI
urn:pki:ca:<scope>or OID1.3.6.1.4.1.66257.1.5.1.
All person credentials (username/password, API token, cookie) take no part in role or permission authority.
2. Authentication input → effective role
| Input | Effective role | Permissions / scope source |
|---|---|---|
| mTLS management cert | OU → role (up to superadmin) | cert PA, cert SAN/OID |
Authorization: Basic (user+pass) |
always operator |
operator defaults; scope only from bound operator cert |
X-Auth-Token / Bearer / cookie |
always operator |
same |
| AIC certificate (delegated agent) | permissions = PA ∩ AIC capabilities |
restricted intersection |
| Trusted-gateway delegation headers (B1/B2) | per certificate after trusted_gateway_ous check |
certificate claims |
Implementation (cmd/pki/serve.go):
resolveAPIToken / resolveBasicAuth:
Role → fixed "operator"
Permissions → getRolePerms("operator")
CAScopes → nil (not injected; scope derived from the bound operator cert)
3. Superadmin is certificate-only (gate on the highest authority)
Management (m-*) certificates are issued only by the superadmin role with
an mTLS client certificate in hand:
POST /api/v1/certs { profile: "m-*" }
├─ no mTLS client cert (TLS.PeerCertificates empty)
│ → 401 api.auth_required
├─ cert role != superadmin
│ → 403 api.management_mint_denied (operator & others hard-excluded)
└─ role = superadmin → mint (optionally with ca_scope)
- operator and every other role are hard-excluded from the management sub-CA.
- The operator's management-mint capability is marked deprecated (scheduled for removal); this gate is the fail-closed front line.
- Scope is written CA-side (not self-declared by the requester).
4. Limitations of username/password (must-read for auditors)
These are deliberate security boundaries — hard limits of every account credential:
- The DB role column does not authorize anything. Even if an account's
rbac_users.roleis configured assuperadmin, Basic/Token authentication still yields effective roleoperator. - An account password cannot elevate anything. Measured: operator cert +
the superadmin account password (
alice:<RBAC_ADMIN_PASS>) requesting a management mint →403; requesting a superadmin-only endpoint (PUT /api/v1/admin/config) →403. - Scope is not injected by the account. Account-declared CA scopes are not used as an authorization basis; only the bound operator certificate derives scope.
- Passwords are valid for: identity attribution, audit trail, operator-cert binding matching — nothing more.
Operational consequences:
- To grant a person anything above operator, issue the matching management certificate; a password cannot do it.
- If an account table shows a "superadmin" role name, that account does not have superadmin capability.
- Certificate private keys (
management/users/private/) are the true secret; the deploy script enforceschmod 600.management/users/certs/*.pemare public certificates and must never be used as--keymaterial.
5. CA scope
- Only certificate-sourced scope is trusted (SAN URI / OID 1.3.6.1.4.1.66257.1.5.1),
written CA-side via the
scopeparameter. - Enterprise mode + no scope → deny (fail-closed); simple mode + no scope → allow.
- Non-certificate resolvers do not inject
cas:scope:*permissions (resolveAPITokenno longer does).
6. Route-level authorization (fail-closed)
- With
routes.jsonconfigured but unreadable: panic at startup (no prior table) or keep the previous table (on reload) — never fall back to the lax embedded table. - The embedded default (
internal/serve/routes_default.json) is a strict least-privilege table kept in sync with reporoutes.json; the deploy script asserts "no drift". - Public (no-auth) path minimum:
/healthz /readyz /metrics /tsa /ocsp /acme/ /api/v1/users/login|info|logout /api/v1/session /api/v1/version.
7. Role × permission matrix (core)
| Role | ca:create/delete | cert:issue/revoke/renew | user:manage | config:write | webhook:manage | log:read/report |
|---|---|---|---|---|---|---|
| superadmin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| admin | — | issue/revoke/renew | — | — | ✓ | ✓ |
| operator | — | issue/revoke/renew | — | — | — | ✓ |
| revoker | — | revoke | — | — | — | ✓ |
| auditor | — | — | — | — | — | read |
| readonly | — | — | — | — | — | — |
| auto-renew | — | renew | — | — | — | ✓ |
| reporter | — | — | — | — | — | view |
Endpoint-level matrix: see routes.json (drift-checked against the embedded default each deploy).
8. Audit guide
- Replay the matrix and P0 assertions in one command:
scripts/verify-rbac-api.sh(with--deployfor a one-shot environment). - Results archive:
docs/security/zh/rbac-verification-2026-08-28.md. - Commits covering these boundaries:
f7355b2(cert-scope wiring),117e35b(management hard-exclusion + route fail-closed + strict default),cc46b20(superadmin certificate-only; accounts always operator).
9. Trust boundary
- TLS enforces
RequireAndVerifyClientCert+ configured CA trust pool (no certificate → no request). - Management certs issued by this PKI, status checkable (DB records + CRL);
ExtractAdminScopeis enforced across the chain. - Assumptions: root CA private key protected;
management/users/private/is 0600; policy tables (routes/authz) are PKCS#7-signed withpolicy_signingwhere enabled (missing signature → load refused).