Four-eyes / maker-checker approvals¶
Secsy PKI can hold high-risk operations at a fail-closed gate until a configurable number of distinct approvers — never the requester — sign off. This implements separation of maker (who requests) from checker (who approves), a standard control for high-assurance certificate authorities.
The gate is disabled unless approvals.enabled is set, so existing
deployments are unaffected until they opt in.
Roles¶
- The
approverrole (capabilityapproval:approve) may approve or reject requests. It is deliberately separate from the roles that request the guarded operations (e.g.issuer,admin): separating maker from checker is the whole point, and the gate additionally denies self-approval by identity (the requester can never approve their own request, even if they holdapprover). - The
auditorrole has read-only visibility into the queue (approval:read).
Guarded operation classes¶
| Class | Guards |
|---|---|
ca.create |
Creating a root or intermediate CA |
ca.rotate |
Intermediate-CA key rotation |
ca.retire |
Retiring a superseded intermediate CA |
revocation.bulk |
Bulk certificate revocation |
secret.kek_rotate |
Secret-layer KEK rotation |
cert.issue |
Operator/API leaf issuance (per-profile) |
profile.change |
(reserved) issuance-profile changes |
escrow.policy |
(reserved) key-escrow-policy changes |
Configuration¶
approvals:
enabled: true
default_threshold: 2 # distinct approvers for any guarded class
request_ttl_hours: 72 # a pending request expires after this window
thresholds: # per-class overrides (0 = leave that class ungated)
cert.issue: 2
ca.create: 2
revocation.bulk: 3
- A bare
enabled: true(no thresholds) guards every class at threshold 2. - When only per-class
thresholdsare given, unlisted classes stay ungated, so you can guard just a subset (e.g. onlycert.issue). - Tightening the policy never weakens an in-flight request: each request
snapshots its
required_approvalsat creation time.
Per-profile manual issuance approval (cert.issue)¶
Routine leaf issuance normally executes immediately. For high-assurance,
wildcard, or otherwise sensitive certificate shapes, mark the profile with
require_approval: true so operator/API issuance under it is held for approval:
profiles:
- name: wildcard-tls
description: "Wildcard TLS certificate — held for manual approval"
key_usages: [digitalSignature, keyEncipherment]
ext_key_usages: [serverAuth]
default_validity_days: 90
max_validity_days: 397
require_approval: true
Enforcement requires both require_approval: true on the profile and the
cert.issue class guarded (via approvals.enabled + a positive threshold). When
the gate is off, require_approval is inert and issuance proceeds immediately.
Flow¶
- A caller
POST /api/ca/{id}/issueunder arequire_approvalprofile. Instead of a certificate, the server validates the CSR, parks the request, and returns202 Acceptedwith the pending-approval id (also in theX-Secsy-Approval-Idheader) and acertificate_url. No certificate is issued. Audit:cert.issue.pending. - The request appears in the approval queue. Distinct approvers approve it
(
POST /api/approvals/{id}/approve), reusing the same engine that guards the admin operations. Self-approval and repeat votes by the same approver are refused. - Once the threshold is met, fetch the certificate from
GET /api/approvals/{id}/certificate. The server performs the held issuance on the HSM exactly once, records the serial, and returns the certificate; subsequent fetches redeliver the same certificate. Audit:cert.issue.approved. - On rejection or expiry the request is terminal and no certificate is
ever issued. Audit:
cert.issue.denied.
The request fingerprint pins the exact issuance (profile + subject + SANs + requester), so an approval cannot authorize a different certificate than the one requested.
Scope: automated protocol flows bypass the manual gate¶
The manual gate applies only to operator/API-driven issuance — the REST and
gRPC IssueCertificate paths. The automated enrollment protocols —
ACME, EST, SCEP, and CMP — enroll machines and call the issuance engine
directly, so they always bypass the cert.issue gate regardless of a
profile's require_approval flag. This is deliberate: those flows are
machine-to-machine and are governed by their own controls (EAB, mTLS, hardware
key attestation, CAA, rate limits). If you need a profile approved-only for
humans but automatable for machines, that is exactly the behavior you get.
The
secsy-ca issueCLI (which calls the CA manager directly, like theinit-root/issue-intermediatebootstrap commands) is likewise not gated; the CLI's role in this workflow is the approval queue, below.
CLI¶
# List / inspect requests (works for every class, including cert.issue)
secsy-ca approvals list [-status pending] [-class cert.issue]
secsy-ca approvals show <id>
# Approve / reject (a DIFFERENT approver than the requester is required)
secsy-ca approvals approve <id> -approver alice -comment "reviewed"
secsy-ca approvals reject <id> -approver alice -comment "denied"
# Once a cert.issue request is approved, complete and fetch the certificate
secsy-ca approvals certificate <id> # prints the leaf PEM
secsy-ca approvals certificate <id> -chain # leaf + issuer
secsy-ca approvals certificate <id> -out leaf.pem
approvals certificate signs on the HSM, so it builds the CA key provider; the
other subcommands need only the database.
Console¶
The Approvals page lists the queue with Approve / Reject buttons. A
cert.issue request that has reached approved/executed shows a
Certificate button that fetches (and, the first time, completes) the issued
certificate and downloads the PEM.
Audit & metrics¶
- Engine lifecycle events (all classes):
approval.request,approval.approve,approval.reject,approval.execute,approval.expire— in the tamper-evident hash-chained event log. - Issuance-domain events:
cert.issue.pending,cert.issue.approved,cert.issue.denied. - Metric
secsy_cert_issue_approvals_total{result=pending|approved|denied|error}.
Stale requests are retired by a leader-elected background sweep and are also enforced fail-closed at read time.