Certificate policy engine
The policy engine puts project-scoped guardrails on certificate issuance and renewal. 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.
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.
- Renew — manual/API renewal re-evaluates the certificate's current name, key type, issuer, domains, and auto-renew flag. A certificate that was compliant at issue time can fail renewal after the policy is tightened.
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 do run through the renewal path, so they are policy-checked at execution time.
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 issuance and renewal 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.