Policies & approvals
Two governance gates: a project-scoped certificate policy that constrains every new issuance (key types, allowed issuers, SAN globs, naming regex, auto-renew requirement — existing certificates and their renewals are grandfathered), 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 and applies to new issuance only: the request fails with HTTP 422 and a structured violation list before any work is queued. Renewals and reissues of existing certificates are never policy-checked — a policy tightened after issuance does not break certificates already in service. 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. |
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
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 gated only by role (Operator) and, optionally, the download-approval setting.
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.
Org-wide settings enable or disable each gate independently — the toggles live in general settings and there is no per-project override, so flipping one changes behaviour for the whole organisation. Every certificate gate is off by default; the configuration-change gate is the one exception and ships on.
Actions that can be gated (the seven 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.
- Configuration change — operator create/update/delete of targets, target groups (and their members), PathSets, ActionSets, project variables and module configs. On by default, and stored inverted as the
config_change_approval_disabledkill switch. - Distribution — operator link / update / unlink / execute / rollback of a distribution. Off by default.
The last two gate the deployment supply chain rather than the certificate itself, and only the manual operator action is gated — the scheduler's automatic post-renewal distribution is not, because gating it would block renewals. A gated write answers 202 Accepted carrying an approval request instead of the resource; see General settings.
Request states
| State | Meaning |
|---|---|
pending_approval | Awaiting review. (That is the literal value — filtering on pending matches nothing.) |
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 are raised for AD CS (MSCA) issuance only — a stale template cache, a connection not validated in the last 24 hours, or credentials changed since the last successful authentication. Other issuer types skip preflight entirely, so a revoke or renew request carries no blast-radius analysis: an approver reviewing one is not being shown which distributions it would affect. Where a warning does apply, acknowledging it is not optional and the acknowledgement is recorded 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.
Nobody can approve my request
There is no laddered approver model and no "no eligible reviewer" error. A request is reviewable by any project admin (and by org admins and owners). If a request is sitting unactioned, the project simply has no admin — grant one, or have an admin perform the action directly, since admin/owner actions bypass the 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.