Policies & approvals
Two governance gates: a project-scoped certificate policy that constrains every issue / renew (key types, allowed issuers, SAN globs, naming regex, auto-renew requirement), and a request / approve / reject workflow that interposes a human review on sensitive operations.
Certificate policy
One policy per project (singleton — you don't stack policies). Enforcement is synchronous: the issuance / renewal request fails with HTTP 422 and a structured violation list before any work is queued. The policy is cached for 60 seconds; saves take up to that long to propagate to all workers.
An empty policy (or no policy) means no enforcement.
Rules
| Rule | Purpose | Empty value |
|---|---|---|
allowed_key_types | Whitelist of key types: ECDSA-P256, ECDSA-P384, RSA-2048, RSA-3072, RSA-4096. Up to 10 entries. | All allowed. |
allowed_issuer_types | Whitelist of issuer types (acme / msca). | All allowed. |
allowed_issuer_ids | Whitelist of specific ACME-account / MSCA-connection IDs (subject to the type list). | All allowed. |
san_rules.allow | Globs — a SAN matching any allow glob passes. | Allow-by-default. |
san_rules.deny | Globs — a SAN matching any deny glob is rejected. Evaluated before allow. | Deny none. |
san_rules.max_san_count | Cap on number of SANs per cert. | No cap. |
naming_pattern | Go regex; the certificate's human-assigned name must match. | No check. |
require_auto_renew | Rejects certs with auto_renew: false. | Manual renewals allowed. |
allow_key_export | Whether the Download flow may return the private key (PEM / PKCS#12 / zip). | Export allowed. |
Evaluation order
- Resolve the policy for the project (cached, 60 s TTL).
- Key type — reject on mismatch.
- Issuer type, then issuer ID — reject on mismatch.
- SAN rules: max_san_count, then deny globs, then allow globs (if an allow list is set, a SAN not matching any allow is rejected).
- Naming regex — reject if the certificate name doesn't match.
require_auto_renew.- If any step produced a violation, return HTTP 422 with the full list. The UI surfaces violations inline on the issuance form, with the offending rule named.
Example policy
allowed_key_types: ["ECDSA-P256", "ECDSA-P384"]
allowed_issuer_types: ["acme", "msca"]
san_rules:
allow: ["*.example.com", "*.internal.example.com"]
deny: ["*.admin.example.com"]
max_san_count: 10
naming_pattern: "^(prod|stage)-[a-z0-9-]+$"
require_auto_renew: true
allow_key_export: false
Reads: ECDSA only, any configured ACME or MSCA issuer, SANs must live under example.com or its internal subtree but never under admin.example.com, up to 10 SANs per cert, the human-readable name must start with prod- or stage-, auto-renew must be on, private-key download is forbidden.
Configure & live test
- Settings → Certificate Policy (project admin role required).
- Toggle Enable policy.
- Fill the rules. The form has live-test inputs — type a candidate cert name to verify the naming regex and a candidate domain to verify the SAN allow / deny globs before you save. Regex compile errors surface next to the input.
- Save. Cache TTL means changes take up to 60 s to propagate.
Bypass: there is no in-UI bypass. If a policy blocks a legitimate request at 2 a.m., either temporarily relax the specific rule (admin-only, audit-logged), disable the policy + issue + re-enable, or use a different project. Every disable / relax is audit-logged.
Approval workflow
An optional gate that interposes a human review on top of RBAC. RBAC controls who can submit; the approval policy controls whether they execute directly or queue a request that must be approved by a reviewer with the right role.
Per-project settings enable or disable each gate independently. The default deployment leaves all gates off.
Actions that can be gated (the five toggles under Settings → General):
- Issue — a new cert request.
- Renew — manual renewal outside the scheduler.
- Reissue — key rotation.
- Revoke — revocation at the CA.
- Download (key export) — pulling the private key.
Request states
| State | Meaning |
|---|---|
pending | Awaiting review. |
approved | Reviewer accepted; the requester now triggers execution explicitly. |
executing / executed | Action running / ran (success or failure captured on the resulting job). |
execution_failed | The approved action failed to execute (retryable for some error codes). |
rejected | Reviewer declined (comment required). |
expired | Not reviewed within 7 days. |
cancelled | Requester cancelled before review. |
Submit a request
- Attempt the gated action on the UI (e.g. click Revoke on a cert).
- If the gate is on, the dialog shifts from Confirm to Request approval. Fill the reason (required) and acknowledge any preflight warnings.
- Submit. The action is queued; the cert detail surface shows the pending request.
Preflight warnings surface conditions like "this cert has 45 days left — revoking it breaks three live distributions" or "this cert is in a policy-required auto-renew state — revoking breaks policy". Acknowledging isn't optional; approvers see the acknowledgement in the audit trail.
Review a request
- Reviewer opens My Requests → Reviews — every pending request they are empowered to act on.
- Click a row; the full context appears (requester, reason, preflight warnings, target cert, action).
- Approve with an optional comment, or Reject with a required comment.
- On approve, the requester triggers execution explicitly with the Execute button on My Requests. The requester is notified via their configured channels.
Who can approve what
- Reviewers need project admin (or org admin/owner) on the request's project.
- Self-approval is blocked for operators — the approver cannot be the requester.
- Admins and owners auto-approve their own requests — the request is still recorded (audit trail) but doesn't wait for a second person.
TTL & expiry
Requests time out after a fixed 7-day TTL (evaluated lazily when the request is next read). An expired request terminates in expired; the action does NOT execute. The requester can re-submit.
Notifications
approval.requested— fires when a new request enterspending. Route this to a reviewer channel (shared inbox, Slack group).approval.approved/approval.rejected— fires on state transition. Route back to the requester.approval.executed/approval.expired— execution completed / request timed out.
Full lifecycle, execute/retry semantics, and download tokens: Approval workflow.
approval.expired— catches stale requests; useful for metrics.
Configure under Notifications with the same channel + rule pattern as cert events.
Troubleshooting
"Policy violation" but no rule seems to apply
Check the cached age — a recent save may not have propagated yet (60 s TTL). The 422 response names the exact rule.
Naming regex silently rejects everything
Usually a Go-regex-syntax issue (PCRE features like lookaheads aren't supported). The live-test panel surfaces compile errors inline.
"No eligible reviewer" on submit
The org has no user with a role one tier above you on this scope. Either lower the request, add a reviewer, or temporarily disable that approval gate.
Request stuck in approved, action didn't fire
The executing worker is down or the action failed with a system error. Check Jobs — the approval-triggered job carries the approval ID in its payload.