RBACEN
RBAC & Permission System
Overview
The varwof PKI implements a role-based access control (RBAC) system with two modes:
- Simple: Role-based, CA scope optional
- Enterprise: Role-based + mandatory CA scope enforcement
Authentication Chain
Every request is authenticated via one of these methods (in priority order):
1. mTLS client certificate
├── AIC certificate (has AIC extension) → Delegation auth verification
├── Trusted gateway delegation (B2: X-Client-Cert-DER, B1: X-Agent-User)
├── Standard management cert → Role from OU, permissions from PA extension
└── Delegated-Agent cert → Requires valid X-Agent-TTL header
2. X-Auth-Token header / pki_token cookie → DB lookup → "operator" role
3. Authorization: Bearer → Same as X-Auth-Token
4. Authorization: Basic → Argon2id password verification + caching
Cert-First Authorization Model:
- mTLS certificates: Permissions come only from the certificate's PrincipalAuthorization (PA) extension
- Non-certificate auth (token/basic/cookie): Always assigned "operator" role
- AIC certificates:
Permissions = PA grants ∩ AIC capabilities
Hard limits of accounts (audit-relevant):
- Server-side resolvers (
resolveBasicAuth/resolveAPIToken) always yieldRole: "operator"; even ifrbac_users.roleis configured assuperadmin, the DB role column does not participate in authorization. - Scope is not injected by the account; it is derived only from the bound operator certificate (SAN/OID).
- Therefore superadmin is only ever reached via an mTLS management certificate; username+password can never reach a superadmin-level capability (management mint and superadmin-only endpoints both return 403).
- Passwords are valid only for: identity attribution, audit trail, and operator-certificate binding matching.
Roles
Core Roles
| Role | Profile | Scope | Description |
|---|---|---|---|
superadmin |
m-superadmin |
["Management CA"] |
Full access including CA creation/deletion |
admin |
m-admin |
— | All permissions except CA/user management |
operator |
— | — | Cert issue/revoke/renew, CRL, logs |
revoker |
m-revoker |
["*"] |
Certificate revocation only |
auditor |
m-auditor |
— | Read-only: logs, reports, certificates |
readonly |
m-readonly |
— | Minimal read-only access |
console |
— | — | Web console operations |
auto-renew |
m-auto-renew |
— | Certificate renewal only |
reporter |
m-reporter |
— | Report generation and export |
agent |
agent-proxy |
— | AI agent with gateway capabilities |
Management sub-CA hard exclusion
Minting management (m-*) certificates (POST /api/v1/certs, profile=m-*):
- Requires an mTLS client certificate in hand (empty
TLS.PeerCertificates→401 api.auth_required) - Role must be
superadmin(otherwise →403 api.management_mint_denied) - operator and every other role are hard-excluded from the management sub-CA; the operator's legacy management-mint capability is deprecated (planned removal)
- Certificate scope is written CA-side via the
scopeparameter (not requester-declared)
Security model and full verification data (378×2 matrix + P0 probes) live in the
corerepo:docs/security/rbac-security-model.mdanddocs/security/rbac-verification-2026-08-28.md.
Gateway Roles (namespaced gateway:)
| Role | Grants |
|---|---|
gateway-admin |
gateway:* (all gateway operations) |
gateway-reader |
SELECT:* (read-only) |
gateway-writer |
SELECT:*, INSERT:*, UPDATE:* |
gateway-ops |
SELECT:*, INSERT:*, UPDATE:*, DELETE:* |
gateway-ddl |
All DML + DDL:* |
Permissions
32 permission constants in resource:action format:
| Resource | Actions |
|---|---|
ca |
create, delete, list, info |
cert |
issue, revoke, renew, list, export, batch |
crl |
generate |
user |
manage, list, revoke-all |
log |
read, export |
report |
view, export, generate |
config |
read, write |
ra |
approve, reject |
cross-cert |
issue, revoke |
webhook |
manage |
key |
recover |
dns |
manage |
trust |
import, list, delete |
agent |
manage |
swagger |
view |
web |
view |
Permission Matrix
| Role | ca:create | ca:delete | cert:issue | cert:revoke | cert:renew | user:manage | config:write | log:read |
|---|---|---|---|---|---|---|---|---|
| superadmin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| admin | — | — | ✓ | ✓ | ✓ | — | — | ✓ |
| operator | — | — | ✓ | ✓ | ✓ | — | — | ✓ |
| revoker | — | — | — | ✓ | — | — | — | ✓ |
| auditor | — | — | — | — | — | — | — | ✓ |
| readonly | — | — | — | — | — | — | — | — |
| auto-renew | — | — | — | — | ✓ | — | — | ✓ |
| reporter | — | — | — | — | — | — | — | ✓ |
Authorization Modes
Simple Mode
{
"rbac": {
"enabled": true,
"mode": "simple"
}
}
- Role-based permission check only
- CA scope not enforced unless user has a bound scope
- Users with no scope are allowed everything
- Suitable for single-CA deployments
Enterprise Mode
{
"rbac": {
"enabled": true,
"mode": "enterprise"
}
}
- Role-based permission check + mandatory CA scope enforcement
- Users with no scope are DENIED (fail-closed)
- CA scope extracted from operator certificate, DB, or config
- Required for multi-CA deployments
CA Scope
CA scopes restrict which Certificate Authorities a user can operate on.
Scope Sources (evaluated in order)
Operator Certificate (cryptographic binding)
- SAN URIs matching
urn:pki:ca:<scope> - OID extension
1.3.6.1.4.1.66257.1.5.1 - Must pass full validation (valid, unrevoked, issued by this PKI)
- SAN URIs matching
DB
ca_scopescolumn- Comma-separated scope list stored per user
Config file
rbac.ca_scopes{ "rbac": { "ca_scopes": { "operator": ["Client CA", "VPN CA"], "admin": ["*"] } } }Policy
scopefield inauthz.json{ "roles": { "superadmin": { "scope": ["Management CA"] }, "revoker": { "scope": ["*"] } } }
Scope Resolution Logic
- Framework operations (
ca:create/ca:delete) are scope-exempt (superadmin only) - Read-only roles (
auditor,readonly,reporter) are always allowed - Roles with
scope: ["*"]are always allowed - No scope defined:
- Simple mode → ALLOW
- Enterprise mode → DENY
- Scope contains
*→ ALLOW - Extract CA name from request (path, query, or POST body)
- Exact string match against scope list
- Config file fallback match
- No match → DENY
CA Name Extraction
| Source | Pattern |
|---|---|
| URL path | /api/v1/cert/{ca}/{serial}/revoke |
| Query parameter | ?ca=<name> |
| POST/PUT body | {"ca": "<name>"} (peek up to 64KB) |
Route-Level Authorization
Route rules are defined in routes.json (or embedded defaults):
{
"version": "v1",
"public_paths": ["/healthz", "/readyz", "/metrics"],
"rules": [
{
"method": "POST",
"path": "/api/v1/certs",
"permission": "cert:issue",
"description": "Issue certificate",
"ca_scope": true,
"require_role": ["superadmin", "admin"],
"allow_aic": false
}
]
}
RouteRule Fields
| Field | Type | Description |
|---|---|---|
method |
string | HTTP method (* for any) |
path |
string | URL pattern (/api/v1/cert/{ca}/{serial}) |
permission |
string | Required permission |
ca_scope |
bool | Enable CA scope check |
require_role |
[]string | Additional role whitelist |
allow_aic |
*bool | Allow AIC agent access (nil = true) |
max_validity |
string | Max cert validity for issuance |
Path Pattern Matching
| Pattern | Specificity | Example |
|---|---|---|
| Literal | 1000+ | /api/v1/certs |
| Param | 600+ | /api/v1/cert/{ca}/{serial} |
| Single wildcard | 500+ | /api/v1/ca/* |
| Double wildcard | 400+ | /api/** |
Public Paths (bypass all auth)
/healthz,/readyz,/metrics/api/v1/users/login,/api/v1/users/info,/api/v1/users/logout/api/v1/session,/api/v1/version/tsa,/ocsp,/acme/
Operator Certificate Binding
Bind a management certificate to a user account for cryptographic scope definition.
Bind
# CLI
pki user bind-operator-cert --username operator1 --cert operator.pem
# API
curl -X POST -H "X-Auth-Token: <token>" \
http://localhost:8443/api/v1/users/1/operator-cert \
-d '{"cert_pem":"-----BEGIN CERTIFICATE-----\n..."}'
Certificate Validation
The bound certificate must satisfy ALL:
- Valid PEM, parseable X.509
- Management cert (DigitalSignature + ClientAuth + valid OU)
- OU maps to a real role
- Within NotBefore/NotAfter window
- Issued by this PKI (DB record exists)
- Not revoked (status "V")
If validation fails, authentication fails (no silent downgrade).
Scope Derivation
Bound operator cert → ExtractAdminScope(cert)
→ SAN URIs: urn:pki:ca:<scope>
→ OID extension: 1.3.6.1.4.1.66257.1.5.1
→ Overrides DB ca_scopes
Scope is cached for 30 seconds (max 4096 entries).
Policy File Signing
Policy files (authz.json, routes.json) can be signed with PKCS#7 detached signatures.
Configuration
{
"policy_signing": {
"enabled": true,
"ca_file": "/etc/varwof/core/keys/issuing-ca.pem",
"require_admin_ou": true,
"require": true,
"sig_suffix": ".sig"
}
}
Signing
# CLI
pki policy sign --file authz.json --cert admin.pem --key admin.key --out authz.json.sig
# varwof-cli
varwof-cli config.json policy sign --file authz.json --cert admin.pem --key admin.key
Verification
- PKCS#7 detached signature verification
- Admin OU check (
adminorgateway:admin) - Chain verification against configured CA trust pool
- Fail-closed: missing/invalid signature rejects loading
HTTP Middleware Stack
Request
│
▼
1. Body size limit (10MB)
│
▼
2. Rate limiting (per-IP token bucket)
│
▼
3. Public path check → bypass
│
▼
4. TSA/OCSP protocol dispatch
│
▼
5. Route rules engine
├── CORS check
├── authenticate()
│ ├── mTLS certificate
│ ├── Token/Cookie
│ └── Basic auth (Argon2id)
├── require_role check
├── permission check (user.HasPerm)
├── CA scope check (enterprise mode)
├── AIC identity check
└── Store AuthUser in context
│
▼
6. Handler execution
Delegated-Agent Sessions
| Control | Description |
|---|---|
X-Agent-TTL header |
RFC3339 future timestamp required |
| Max TTL | serve.agent_session_max_ttl (default 24h) |
| Disable | Set agent_session_max_ttl = "0" |
| Trusted gateways | serve.trusted_gateway_ous (empty = reject all) |
Namespace System
| Namespace | Prefix | Roles |
|---|---|---|
| Core | (none) | admin, operator, auditor, etc. |
| Gateway | gateway: |
gateway-admin, gateway-reader, etc. |
| Web | web: |
Reserved for web console |
Wildcard matching: gateway:* matches any gateway role, * matches globally.