Certificate policy engine
The policy engine puts project-scoped guardrails on new certificate issuance. A request that violates the policy is rejected synchronously — nothing is created, no job is enqueued — with HTTP 422 and a structured list of violations. Existing certificates are grandfathered: changing the policy later never affects their renewals.
Configure under Settings → Certificate Policy.
What the policy enforces
Each rule is optional; an empty rule allows everything. All configured rules are checked and every violation is returned (not just the first).
| Rule | Field | Behaviour | Violation code |
|---|---|---|---|
| Allowed key types | allowed_key_types | Key type must be in the list (ECDSA P-256/P-384, RSA 2048/3072/4096). | policy_key_type |
| Allowed issuer types | allowed_issuer_types | Issuer must be acme and/or msca. | policy_issuer_type |
| Allowed issuer IDs | allowed_issuer_ids | Only the listed ACME accounts / MSCA connections may issue. | policy_issuer_id |
| SAN allow patterns | san_rules.allowed_patterns | Every domain must match at least one glob. | policy_san_allowed |
| SAN deny patterns | san_rules.deny_patterns | No domain may match any glob. Deny overrides allow. | policy_san_denied |
| Max SAN count | san_rules.max_san_count | Cap on domains per certificate (0 = no limit). | policy_san_count |
| Naming convention | naming_pattern | Certificate name must match the regex. | policy_naming |
| Require auto-renew | require_auto_renew | Certificate must have auto-renew enabled. | policy_auto_renew |
on_violation currently supports only reject; a require approval mode is shown in the UI as coming soon. A require_distribution field is persisted but not yet enforced.
One policy per project
Each project has at most one policy (a singleton document). The Enabled switch turns enforcement on and off without deleting the rules; deleting the policy removes all guardrails. Saves are versioned — concurrent edits are rejected on version conflict.
Enforcement points
Evaluation is synchronous inside the certificate service, before the certificate is saved or a job is enqueued:
- Issue — both ACME and MSCA creation paths. This is the only issuance enforcement point.
- Renew / reissue — NOT evaluated. An existing certificate is grandfathered under the policy that was in effect when it was created: manual renew, bulk renew, scheduler auto-renew, and reissue all proceed regardless of the current policy. Tightening a policy therefore only gates new certificates — it never breaks the lifecycle of ones already issued. To retire a non-compliant certificate, revoke or delete it and issue a replacement (which is policy-checked).
- Import / re-import — NOT evaluated. Imported (bring-your-own) certificates record an external fact rather than requesting issuance, so the policy engine is deliberately skipped (a "require auto_renew" policy, for instance, can never apply — imported certs never auto-renew). The compensating control is that import and re-import require the Admin project role (not operator), so a non-privileged user cannot use import to activate a certificate that policy would otherwise block.
On violation the API responds 422 Unprocessable Entity:
{
"error": "policy_violation",
"message": "certificate policy violated: 2 rule(s)",
"violations": [
{ "code": "policy_key_type", "field": "key_type",
"message": "Key type RSA-2048 is not allowed. Allowed: ECDSA-P256" },
{ "code": "policy_san_denied", "field": "domains",
"message": "Domain internal.local matches deny pattern *.local" }
]
}
Interaction with approvals
For users who issue directly (Admins/Owners, or when approvals are off), the policy rejects the request before any approval question arises. Two caveats when the approval workflow is enabled for issuance:
- An operator's issue request is stored as
awaiting_approvalwithout policy evaluation, and executing the approved request enqueues the issuance job without re-evaluating. Reviewers should check requests against the policy manually. - Approved renew requests are not policy-checked either — renewals of existing certificates are grandfathered (see Enforcement points).
Glob and regex semantics
SAN patterns are label-by-label globs, case-insensitive. * matches exactly one DNS label — it does not cross dots, and label counts must match:
| Pattern | Matches | Does not match |
|---|---|---|
*.example.com | api.example.com | example.com, a.b.example.com |
*.*.example.com | a.b.example.com | api.example.com |
example.com | example.com only | any subdomain |
*.internal (deny) | db.internal | db.corp.internal |
The naming convention is a standard regex tested against the certificate name (not its domains), e.g. ^(prod|stage)-[a-z0-9-]+$. An invalid regex itself produces a policy_naming_error violation rather than silently passing.
Live test fields
The Settings → Certificate Policy form includes two inline testers that evaluate as you type, using the same matching logic as the backend:
- Domain test (under SAN rules) — shows Denied — matches
<pattern>, Allowed — matches<pattern>, Not allowed — does not match any allowed pattern, or No restrictions. - Name test (under naming convention) — shows Matches pattern / Does not match pattern for the regex.
Use them to sanity-check patterns before saving; nothing is sent to the server.
Cache TTL
Policies are cached in the API process for 60 seconds (including the "no policy" result). After saving, editing, or deleting a policy, allow up to a minute before enforcement reflects the change on new issuance requests.
API
Project-scoped; reading requires Operator, writing requires Admin:
GET /api/v1/projects/{projectId}/certificate-policy
PUT /api/v1/projects/{projectId}/certificate-policy
DELETE /api/v1/projects/{projectId}/certificate-policy
There is no server-side test endpoint — the live testers run in the browser.