Enterprise features: RBAC, audit logging, and configuration¶
This document describes the enterprise access-control, tamper-evident audit logging, and centralized configuration added to secsy-pki (Task 8). These build on the HSM-backed CA (Tasks 4–6) and secret-encryption (Task 7) features.
1. Role-based access control (RBAC)¶
Complementary authorization layers govern every protected endpoint:
| Layer | Scope | Configured in | Answers |
|---|---|---|---|
| Org-wide roles | whole system | rbac: config block |
"what class of user is this?" |
| Tenant roles | one tenant | tenants[].rbac |
"what class of user is this within this organization?" (multi-tenancy) |
| Resource grants | a single CA or key | rbac.grants, /api/grants |
"who owns this particular authority?" (resource grants) |
| Per-CA permissions | a single CA | /api/keys/{id}/permissions |
"may this subject sign with this CA?" |
The layers are additive: a principal's authority is the union, and a grant
can only widen what a role already allows. Resource grants are the general
mechanism for per-CA and per-key delegation — including administering one CA,
which the three-verb per-CA permission model below cannot express. See
per-CA & per-key authorization for the full model, the
role vocabulary, self vs subtree scope, and effective-access review.
The built-in root user (HTTP basic auth) is always a superuser and can be
disabled in production (policy.allow_root_basic_auth: false).
Roles¶
| Role | Capabilities |
|---|---|
admin |
Everything: create/delete CAs, init roots/intermediates, manage groups & permissions, administer the HSM, issue certificates, read all logs, encrypt/decrypt secrets. Equivalent to root. |
issuer |
Issue / renew / revoke certificates and SSH/X.509 sign on any CA (that CA's restriction sets are still enforced), encrypt/decrypt secrets, and read logs. Cannot create/delete CAs, manage access control, or administer the HSM. |
auditor |
Read-only: the audit log, access log, tamper-evident event log (and its verification), and HSM audit log. Cannot perform or authorize any signing or administrative operation. |
Roles are assigned centrally to OIDC subjects (by sub claim or email) and
to group IDs. A user's effective roles are the union of its subject and
group assignments, resolved at authentication time and attached to the request
identity. Unknown role names are rejected at startup.
rbac:
subjects:
"1a2b3c-oidc-subject": [admin]
"auditor@example.com": [auditor]
groups:
"group-uuid-for-pki-ops": [issuer]
Capability model¶
Endpoints check a coarse capability (cert:issue, audit:read, ca:manage,
ca:configure, rbac:manage, hsm:manage, secret:encrypt,
secret:decrypt, token:manage, server:profile). admin/root satisfy all of
them. One capability — resource:delegate — is reachable only through a
resource grant: it is the right to hand one specific CA or
key onward, and no platform or tenant role confers it. token:manage (mint/revoke API tokens) and server:profile (capture
runtime pprof profiles — a raw dump of process memory, so as sensitive as
hsm:manage) are admin-only: no lesser role grants them. For signing
endpoints,
access is granted by either the org-wide cert:issue capability or a per-CA
SIGN_CERTIFICATE grant — so existing per-CA delegation keeps working
unchanged, and an org-wide issuer no longer needs a grant on every CA.
2. Tamper-evident audit logging¶
Every security-sensitive operation is recorded in an append-only,
hash-chained event log (event_log table), capturing who, what, when, which
target, and the result (success / denied / error). Denied attempts are
logged too, which is essential for detecting probing.
Recorded actions include: CA creation, root/intermediate initialization, CA deletion, certificate issue/renew/revoke, SSH and X.509 signing, secret encrypt/decrypt, permission grant/revoke, and HSM provision/factory-reset.
How tamper-evidence works¶
Each entry stores:
- Every field is length-prefixed before hashing, so no rearrangement of characters across field boundaries can forge a colliding entry.
prev_hashlinks each entry to its predecessor; the first entry anchors to a fixed genesis hash.- The
seqsequence number is gap-free and assigned by the server under a mutex inside the same transaction that reads the previous hash, so the chain stays consistent even under concurrent writers.
Any modification, deletion, or reordering of a historical entry breaks the chain
from that point forward. audit.VerifyChain (and the GET /api/events/verify
endpoint) recompute the chain and report the sequence number of the first
inconsistency.
The chain alone cannot prove the log ever extended further than it does now — a
writer with store access could drop the newest entries or re-seal a rewritten
history. Enabling audit-chain anchoring (audit.anchor config) closes that
gap: the chain head is periodically bound into an RFC 3161 timestamp token
(internal HSM-backed TSA or an external TSA URL), persisted in audit_anchors,
and validated by secsy-ca audit verify. See the
Audit-chain anchoring runbook section.
Endpoints (require audit:read — admin or auditor)¶
| Method & path | Purpose |
|---|---|
GET /api/events |
Paginated event log (newest first); ?action= and ?actor= filters |
GET /api/events/stream |
Live feed of new events over Server-Sent Events; ?action= and ?tenant= filters |
GET /api/events/verify |
Verify chain integrity; returns 200 if intact, 409 if tampered |
GET /api/audit-log |
Legacy per-signature certificate audit log |
GET /api/access-log |
HTTP access log |
Live event feed (Server-Sent Events)¶
GET /api/events/stream is the real-time companion to GET /api/events: instead
of paging the historical log, it streams each event as it is sealed. Because
every event — from HTTP handlers, background jobs, and the ACME/EST/SCEP/CMP
protocol servers alike — is appended through the single chokepoint
database.DB.AppendEvent, a hook there fans every hash-chained event out to
connected subscribers identically. The operator console's Audit → Live tail
view consumes it.
- Scope. Authorization and tenant scoping are identical to
GET /api/events:audit:readis required, a platform operator sees every tenant (optionally narrowed with?tenant=), and a tenant-scoped auditor is confined to its own tenant's events — the same isolation guarantee, enforced by the subscriber filter rather than a SQLWHEREclause.?action=narrows to one audit action. - Wire format.
Content-Type: text/event-stream. Each event is one SSE frame withevent: audit, anid:carrying the event's chain sequence number, and adata:payload holding the same event object the listing endpoint returns.: heartbeatcomment lines are sent periodically to keep the connection alive. - Liveness over completeness. The feed targets humans watching the console,
not durable machine delivery (that is the SIEM export below). A subscriber that
cannot keep up has its oldest undelivered events dropped and receives an
event: lagframe naming how many were dropped, rather than ever blocking the audit-append hot path. For a guaranteed-complete, at-least-once copy of the log, use the SIEM export; to fill a gap the live feed reported, refreshGET /api/events. Drops are counted by thesecsy_event_stream_dropped_totalmetric (see Observability).
Exporting the log to a SIEM¶
The event log can be streamed to external syslog/CEF/webhook collectors and
verified or exported from the CLI (secsy-ca audit verify / audit export).
See Audit log export to SIEM.
3. Centralized configuration¶
All governance lives in one YAML file (config.yaml) alongside the existing
server, database, OIDC, key-provider, PKCS#11/HSM, and secret settings:
rbac— role assignments (above).policy— system-wide guardrails:max_cert_validity_days— global cap on issued end-entity validity (0 = uncapped; per-profile / per-CA limits still apply).require_reason— require a reason on sign requests that carry the field.allow_root_basic_auth— enable/disable the built-in root user.profiles— custom certificate profiles layered over the built-ins (server,client,server-client,code-signing,email). A custom profile with a built-in's name overrides it, letting an operator tighten validity or add issuance shapes without a code change. Referenced key usages are validated at startup.
policy:
max_cert_validity_days: 90
require_reason: true
allow_root_basic_auth: false
profiles:
- name: short-lived-client
description: "Ephemeral mTLS client certificate"
key_usages: [digitalSignature]
ext_key_usages: [clientAuth]
default_validity_days: 7
max_validity_days: 30
See server/config.yaml for a fully-commented example.
Security notes¶
- Least privilege by default.
auditorcannot sign;issuercannot administer CAs or the HSM. Secret encrypt/decrypt now require an explicit capability rather than mere authentication. - Non-repudiation. The event log binds each action to the authenticated subject, the roles it held at the time, and the client IP.
- Defense in depth. Application-level hash chaining complements the device-level HSM audit log (YubiHSM), giving tamper-evidence even if the database is writable by an attacker who cannot forge SHA-256 preimages.
- Fail-safe logging. A failure to append an audit event is logged as a warning but never silently changes an authorization decision; the chain's integrity is independent of any single external side effect.
- Systematic authorization coverage. A table-driven regression matrix
(
server/internal/handlers/authz_matrix_test.goand the gRPC mirrorserver/internal/grpcapi/authz_matrix_test.go) asserts, for every registered route/RPC, that an unauthenticated caller gets401, a capability-lacking principal gets403, a cross-tenant principal is refused with no data leak, and a correctly-capable principal succeeds. The suite fails the build if a newly registered route has no declared RBAC/tenant intent, so access-control coverage cannot silently regress. See authz-regression-matrix.md.