Security model¶
ZeroEcho PKI is built around a single, uncompromising principle: the server authorizes every request on its own merits. There are no trusted clients, no sessions, and no user interface that can grant access the server would otherwise refuse. This page describes the trust boundaries, the authentication and authorization mechanisms, the dual-control and break-glass workflows, the audit trail, and the data-view redaction levels that together enforce least privilege.
It is a companion to the administrative API (which carries these requests), the configuration reference (which defines authentication and disclosure defaults), and the CLI reference.
Trust boundaries¶
The server sits at the center of a small number of explicit boundaries:
flowchart LR
admin[Administrator / CLI] -->|mutual TLS| edge
edge[Reverse proxy or direct TLS] -->|authenticated transport + forwarded identity| server
relying[Relying party] -->|OCSP / public repo| server
acmeclient[ACME client] -->|JWS over TLS| server
server[(ZeroEcho PKI server)] --> store[(Store + audit + keyring)]
- Client ⇄ server. Every administrative connection presents a client
certificate. In
DIRECT_MTLSthe server maps the TLS peer directly. InTRUSTED_REVERSE_PROXYan independently authenticated proxy terminates the client TLS and forwards the validated client certificate; the proxy's own transport certificate must belong to the explicitly trusted-proxy principal set, and proxy and end-client identities can never overlap. - Server ⇄ store, audit, and keyring. Secrets (keyring unlock, TLS store passwords) are supplied only as environment-variable names in configuration; the process reads the values at startup. The configuration file itself holds no secrets.
- Relying parties. OCSP and the public repository are served on a separate, read-only listener with its own bounds and its own optional-client authentication.
Per-request authentication — no cookies, no sessions¶
There is no login endpoint, no session cookie, and no bearer token that stands in for a user. Authentication happens on every request from the client certificate:
- The TLS peer (or the forwarded client certificate) is matched against the configured client-certificate mappings by a cryptographic commitment — certificate SHA-256, subject-public-key-info SHA-256, or issuer-and-serial SHA-256. All present commitments must match.
- A match resolves to a server principal. No match is a hard
401authentication failure. - In
DIRECT_MTLSmode, a request that carries a forwarded-identity header is refused outright (400), because only a trusted proxy may forward identity.
Because identity is re-established per request, revoking or disabling a principal takes effect immediately — there is no session to expire.
Role-based access control¶
Every authenticated request is evaluated by a deterministic, side-effect-free, default-deny authorization engine before any work is done. The engine reads an immutable snapshot of the principal's grants for that single decision; it never calls out to external code. Explicit deny always wins over any allow.
Principals, roles, and grants¶
- A principal is a stable server identity (an administrator, a service, a proxy). Principals can be enabled or disabled.
- A role assignment binds a principal to a role template within a scope, optionally with an expiry. Role templates package grants for common duties.
- A grant is the atomic unit of authorization. Each grant is an immutable, scoped tuple:
| Grant field | Meaning |
|---|---|
effect |
ALLOW or DENY (deny takes precedence). |
action |
The exact permitted action (see the action vocabulary below). |
resourceType |
The kind of resource the action applies to. |
scope |
Realm, and optional authority, issuer, profile, and OCSP-responder dimensions. |
relationship |
OWN (only objects the principal owns) or ANY. |
dataView |
The maximum data view the grant confers. |
conditions |
Zero or more closed conditions (see below). |
expiresAt |
Optional mandatory upper bound for a temporary grant. |
enabled |
Whether the grant participates in decisions. |
Decision outcomes¶
The engine returns one safe decision code that never discloses the existence of a protected object:
ALLOWED, NO_MATCHING_GRANT, EXPLICITLY_DENIED, OUTSIDE_REALM,
OUTSIDE_AUTHORITY_SCOPE, INSUFFICIENT_DATA_VIEW, APPROVAL_REQUIRED,
GRANT_EXPIRED, PRINCIPAL_DISABLED, CONDITION_NOT_SATISFIED.
Action vocabulary¶
Actions are semantically separated so that a grant confers exactly one capability and no more — reading metadata does not imply reading content or PII, and issuing does not imply revoking. The vocabulary is a closed, stable set grouped by domain:
| Domain | Representative actions |
|---|---|
| Realm & server | REALM_READ, SERVER_CONFIGURATION_READ/UPDATE, FORWARD_AUTHENTICATED_CLIENT_IDENTITY |
| Identity & RBAC | PRINCIPAL_MANAGE, ROLE_MANAGE, PERMISSION_GRANT |
| Authorities & issuers | AUTHORITY_LIST/READ/CREATE/…, ISSUER_CREATE/ROTATE/RETIRE, CA_CHAIN_DOWNLOAD |
| Profiles & policy | PROFILE_READ/REGISTER/VALIDATE/ACTIVATE/DEACTIVATE, POLICY_READ/UPDATE, X509_BINDING_READ |
| Requests | REQUEST_SUBMIT, REQUEST_READ_OWN/ANY, REQUEST_APPROVE/REJECT/CANCEL |
| Certificates | CERTIFICATE_ISSUE/RENEW/REKEY, CERTIFICATE_READ_METADATA/CONTENT/PII, CERTIFICATE_REVOKE/HOLD/RELEASE_HOLD, CERTIFICATE_CONFIRM |
| Revocation & status | REVOCATION_HISTORY_READ, CRL_GENERATE/PUBLISH/DOWNLOAD, OCSP_ADMINISTER, OCSP_STATUS_QUERY |
| Publication | PUBLICATION_REGISTER/READ/PROCESS/RETRY/RECONCILE |
| Audit & backup | AUDIT_READ_REDACTED/FULL/PII, AUDIT_EXPORT, AUDIT_INTEGRITY_VERIFY, BACKUP_EXPORT/VERIFY, RESTORE_EXECUTE |
| Disclosure & ACME | DISCLOSURE_READ/CHANGE, DISCLOSURE_CAPABILITY_ISSUE/REVOKE, REPOSITORY_ALIAS_READ/MANAGE, ACME_DIRECTORY_READ/MANAGE, ACME_ACCOUNT_READ/MANAGE |
Resource types (REALM, AUTHORITY, ISSUER, PROFILE, CERTIFICATE,
REVOCATION, STATUS_OBJECT, PUBLICATION, AUDIT, DISCLOSURE, CAPABILITY,
APPROVAL, BREAK_GLASS, PRINCIPAL, ROLE, GRANT, and others) are likewise
distinct from PKI authority ownership: possessing an object reference is never
authorization to act on it.
The complete mapping from every operation ID to its required action, resource type, and whether high-risk approval applies is in the Operation permissions reference.
Grant conditions¶
A grant may attach closed, non-programmable conditions that must all be satisfied for the grant to allow:
| Condition | Effect |
|---|---|
REASON_REQUIRED |
The request must carry an operator justification. |
APPROVAL_REQUIRED |
A valid approval must be present (drives dual control). |
REQUESTER_APPROVER_SEPARATION |
The acting principal must differ from the recorded requester. |
AUTHORITY_ACTIVE |
The target authority must be active. |
PROFILE_MATCH_REQUIRED |
The request must be profile-scoped. |
Approvals and separation of duties¶
Some operations require multi-party approval before they run. Approval treatment
is expressed per operation as a category: HIGH_RISK (a realm-wide dual-control
category for administratively sensitive operations such as authority creation),
or the issuance categories ISSUANCE_SINGLE and ISSUANCE_THRESHOLD (selected
dynamically from the resolved certificate profile). Ordinary operations are
NONE, but a grant's APPROVAL_REQUIRED condition can still require approval —
for example, conditioning a revocation on dual control without classifying every
revocation as high risk.
An approval policy is an N-of-M rule:
| Policy field | Meaning |
|---|---|
threshold |
The number of distinct approvals required. |
eligibleApprovers / requiredRoleTemplateIds |
Who may approve (by principal, or by holding a required role in scope). |
requesterSeparation |
When true, the requester may not approve their own request. |
lifetime |
How long the approval request stays valid. |
justificationRequired |
Whether each decision must carry a non-blank justification. |
The workflow is bound to the exact operation by a cryptographic operation commitment (a digest over the operation, its arguments, and its scope):
sequenceDiagram
participant R as Requester
participant S as Server
participant A as Approver(s)
R->>S: POST operation (no approvalId)
S-->>R: 409 APPROVAL_REQUIRED { operationCommitment }
R->>S: approval.request (binds commitment)
A->>S: approval.decide APPROVE (justification)
Note over S: threshold met → APPROVED
R->>S: POST identical operation + approvalId
S->>S: claim approval, re-verify commitment
S-->>R: SUCCEEDED
An approval record moves through the states PENDING → APPROVED → EXECUTING →
EXECUTED, or terminalizes as REJECTED, EXPIRED, or CANCELLED. The server
re-verifies at execution time that the approval binds precisely the operation
being run, that decisions came from distinct eligible approvers, and — when
requesterSeparation is set — that no approver is the requester. Every request,
decision, expiry, cancellation, and execution is written to the audit trail.
Break-glass¶
For genuine emergencies, a break-glass record grants a narrowly scoped, time-bounded, single-action allow to a named principal, created and then explicitly activated through the admin API. Break-glass authority is subject to the same authorization engine; when a decision is allowed by a grant originating from an active break-glass record, the decision is flagged as such so that its use is unmistakable in the audit trail. Break-glass records are created, activated, inspected, listed, and revoked as ordinary high-risk control operations, and are themselves auditable.
The audit trail¶
Administrative and PKI activity is written to a cryptographically chained, integrity-verifiable audit trail. Each record links to its predecessor, so any insertion, deletion, or modification breaks the chain and is detectable.
- Verify integrity.
audit.verify(actionAUDIT_INTEGRITY_VERIFY) checks the chain, optionally against an expected head — a{ sequence, recordHash }pair a client recorded earlier — to confirm the trail has not been rewritten or truncated. - Search and export.
audit.search(actionAUDIT_EXPORT) queries by category, action, time window, object, or principal. - Tiered inspection. Reading audit detail is separated into three explicit actions and views, so that reading metadata never implies reading personal data (see below).
Data-view redaction¶
Read access is graded by an explicit data-view level, and a grant confers a maximum view. A grant for a lower view can never read a higher one; the only implied widening is that a full-metadata grant also satisfies a redacted-metadata request.
| Data view | Confers |
|---|---|
METADATA_REDACTED |
Redacted metadata only. |
METADATA_FULL |
Full metadata (also satisfies a redacted-metadata request). |
CONTENT_FULL |
Object content. |
PII_FULL |
Personally identifiable information. |
Audit inspection maps directly onto these: audit.view.redacted,
audit.view.full, and audit.view.pii require AUDIT_READ_REDACTED,
AUDIT_READ_FULL, and AUDIT_READ_PII respectively.
Object disclosure to relying parties is governed separately by a per-object
policy, with realm-wide defaults per object class (rootCa, intermediateCa,
caChain, crl, leaf, sensitiveLeaf). The disclosure levels are:
PUBLIC, PUBLIC_UNLISTED, AUTHENTICATED, OWNER_ONLY, RESTRICTED, and
NOT_PUBLISHED.
Non-public objects can be retrieved with a short-lived capability credential issued through the admin API; a request for an object the caller may not see returns not found, never a disclosure of its existence.
Least privilege and the web console¶
Two design consequences follow directly from the model:
- Least privilege by construction. Because actions, resource types, and data views are all separated and grants are scoped and optionally time-bounded and conditioned, a principal can be given exactly the capability it needs — for example, "issue certificates under one authority, using one profile, with an operator justification" — and nothing more.
- The web console is never an authorization boundary. Any user-facing console is only a client of the administrative API. It presents its own client certificate and is subject to the same per-request authentication and authorization as any other client. It cannot see, do, or reveal anything the server would not authorize for the underlying principal. Hiding a button in a UI is a convenience, never a control; the control is the server's authorization decision on every request.