Approval workflow
A four-eyes gate on top of RBAC: when a gate is on, operators don't execute the operation directly — they file a request that a project admin approves, and only then does it run. This page deepens the approval overview.
What can be gated
Five independent toggles under Settings → General → Approval Requirements (org-wide, stored in general settings):
| Toggle | Setting | Gates |
|---|---|---|
| Issue | require_approval_for_issue | New certificate requests |
| Reissue | require_approval_for_reissue | Reissue with new parameters / key rotation |
| Renew | require_approval_for_renew | Manual renewals |
| Revoke | require_approval_for_revoke | Revocation at the CA |
| Download | require_approval_for_download | Private-key export |
All gates are off by default. Scheduler-driven auto-renewals are not affected — the gates sit on the API operations.
Lifecycle
| Status | Meaning |
|---|---|
pending_approval | Awaiting a reviewer. |
approved | Accepted — waiting for an explicit Execute. |
executing | Operation in flight. |
executed | Ran successfully (job outcome tracked on the job itself). |
execution_failed | Operation errored; retryable failures can go back to approved. |
rejected | Declined with a reason. |
cancelled | Withdrawn by the requester or an admin before execution. |
expired | Not completed within 7 days of creation. |
Only one open request may exist per certificate + operation — a duplicate submit is rejected until the first request reaches a terminal state.
Who approves — and who bypasses
- Approve / reject requires the project admin role (or higher) on the request's project.
- Self-approval is blocked at the service layer — the approver cannot be the requester ("cannot approve your own request").
- Admins and owners auto-approve: when a privileged user submits a gated operation, the request is created directly in
approved(with themselves recorded as approver) — they still go through the request/execute flow, so the audit trail is uniform, but no second person is required.
Net effect: the gate adds a second pair of eyes for operator-initiated actions; admin/owner actions are recorded but not blocked.
Execution
Approval does not auto-run the operation. After a request reaches approved, the requester or a project admin clicks Execute (or calls the execute endpoint). Execution:
- enqueues the underlying job for issue / reissue / renew / revoke (the job ID is linked from the request), or
- mints a one-time download token for download requests.
If execution fails, the request lands in execution_failed with an error code. The UI offers Retry for transient codes (JOB_QUEUE_ERROR, CA_ERROR, STATUS_CONFLICT); retry moves the request back to approved and executes again — no re-approval needed.
Expiry
Every request gets a fixed 7-day TTL at creation (not configurable). There is no background sweep — expiry is enforced lazily: any approve / reject / execute / cancel attempt on an overdue request atomically flips it to expired, emits approval.expired, and fails the attempted action. Expired requests never execute; re-submit if still needed.
Approved downloads
Private-key export through the gate ends in a token exchange rather than a direct file response:
- Requester asks to download; a
downloadapproval request is created (or auto-approved for admins). - A project admin approves.
- Requester (or admin) executes — the response contains a raw download token.
- The browser fetches
GET /api/v1/downloads/{token}immediately.
Token properties: 60-second TTL, single use (atomically consumed), bound to the executing user, and stored only as a SHA-256 hash. Expired, reused, or wrong-user tokens all fail identically. See Certificate download for formats.
Preflight warnings
Independent of the approval gates, MSCA issuance runs preflight checks and can pause submission with warnings that the requester must explicitly confirm (re-submit with the confirmation flag):
- template cache older than 30 days — consider syncing templates,
- connection not validated in over 24 hours,
- credentials changed after the last successful authentication.
Other issuer types skip preflight. The confirmation is part of the request, so approvers review a warned-and-acknowledged submission.
My Requests page
My Requests in the main navigation:
- My Requests view — everything you submitted, filterable by status (All / Pending / Approved / Rejected / Executed / Failed / Expired / Cancelled). Rows expand to show reason, reviewer, rejection comment, and execution errors.
- Pending Reviews view (project admins only) — every
pending_approvalrequest you can act on, with a live count badge.
Actions offered per status (the API additionally lets project admins execute / retry / cancel — see endpoints):
| Status | Requester (My Requests) | Project admin (Pending Reviews) |
|---|---|---|
pending_approval | Cancel | Approve · Reject · Cancel |
approved | Execute · Cancel | view only |
execution_failed | Retry (if retryable) + error detail | view only |
executed | Link to the resulting job | view only |
Notifications
Route these through notification rules:
| Event | Fires when |
|---|---|
approval.requested | A request enters pending_approval (not emitted for auto-approved requests). |
approval.approved | A request is approved — including admin auto-approval at submit time. |
approval.rejected | A reviewer rejects. |
approval.executed | Execution completed successfully. |
approval.expired | A stale request is flipped to expired. |
Payloads carry operation_type, primary_domain, and the requester / reviewer display names.
API endpoints
All under /api/v1, authenticated. The action endpoints are rate-limited to 10 approval actions per user per minute.
| Method | Path | Who |
|---|---|---|
| GET | /approval-requests/my | Any user — own requests |
| GET | /approval-requests/:id | Any user in the org |
| POST | /approval-requests/:id/approve | Project admin (not the requester) |
| POST | /approval-requests/:id/reject | Project admin |
| POST | /approval-requests/:id/execute | Requester or project admin |
| POST | /approval-requests/:id/retry | Requester or project admin |
| POST | /approval-requests/:id/cancel | Requester or project admin |
| GET | /downloads/:token | Holder of a valid, unused token (user-bound) |
| GET | /projects/:projectId/approval-requests | Project admin — review queue |
Troubleshooting
"an open request already exists for this certificate"
One open request per certificate + operation. Find it on My Requests and cancel or complete it first.
Approved, but nothing happened
Approval doesn't execute. Open My Requests, find the approved row, click Execute.
"cannot approve your own request"
Separation of duties — another project admin must review it. (If you're an admin submitting your own operations, they auto-approve at submit time instead.)
Download token "expired" seconds after execute
The token lives 60 seconds and works once, for the user who executed. Execute again to mint a fresh token and let the browser follow it immediately.