Policies
Generated from core/api/router.go. Edit the router, not this file.
| Method | Path | Who can call it |
|---|---|---|
GET | /api/v1/policies | Any authenticated user |
POST | /api/v1/policies | Operator, admin |
GET | /api/v1/policies/:id | Any authenticated user |
PUT | /api/v1/policies/:id | Operator, admin |
DELETE | /api/v1/policies/:id | Admin |
Rule types, all of which are evaluated:
rule_type | rule_config |
|---|---|
key_size | {"rsa_min_bits": 3072, "ecdsa_min_bits": 256} |
key_type | {"allowed_key_types": ["ECDSA", "Ed25519"]} or {"forbidden_key_types": ["RSA"]} |
max_lifetime | {"max_days": 90, "min_days": 30} |
ca_restriction | {"allowed_providers": ["acme", "vault"]} |
naming | {"allowed_suffixes": ["example.com"], "forbidden_patterns": ["*.internal"], "allow_wildcards": false, "max_sans": 50} |
severity is INFO, WARNING, or BLOCK. Only BLOCK refuses the request; the rest are returned in the issuance response.
domain_pattern scopes a policy — * matches everything, *.example.com matches any subdomain. A policy applies when any requested domain matches.
Key sizes are per algorithm
rsa_min_bits and ecdsa_min_bits are separate because the numbers are not comparable: 256 is a strong ECDSA key and a broken RSA one. A single floor cannot express both, and the rule's previous answer — check RSA, ignore everything else — left elliptic keys with no floor at all.
min_key_size is still accepted and still means RSA bits, so policies written against the original shape keep working.
Ed25519 has one size. A floor cannot be violated, so key_size does not judge it. Use key_type to permit or forbid the algorithm itself.
Naming
allowed_suffixes matches on a label boundary: example.com admits example.com, shop.example.com and *.example.com, and refuses evil-example.com. Every offending name in a request is reported, not just the first.
allow_wildcards is only applied when present. A naming policy that does not mention it permits wildcards, so a rule written to cap max_sans does not ban them as a side effect.
A policy never silently does nothing
A rule type this build cannot evaluate, a rule_config that will not parse, and a configuration that sets no constraint each produce a violation at the policy's own severity. Failing open is the one behaviour a security control must not have: an operator who wrote "ECDSA only, BLOCK" and got a row in a table with no effect had no way to tell it apart from compliance.
approval_required is no longer accepted on create. There is no approval workflow in this build — no approvals table, no endpoint to approve or reject — so a policy claiming to gate on approval was promising a control that did not exist. The column still permits the value, because rows may already exist; the engine now reports them rather than passing them.
Policy is evaluated on issuance only, not on renewal.
Endpoint detail
GET /api/v1/policies
| Who can call it | Any authenticated user |
| Handler | policyHandler.List |
| Display token | Readable by an unattended screen |
Responses
| Status | Body |
|---|---|
200 | An object with data (Policy[]), total |
500 | { "error": … } |
Example request
curl -X GET 'https://certpilot.example.com/api/v1/policies' \
-H 'Authorization: Bearer <token>'POST /api/v1/policies
| Who can call it | Operator, admin |
| Handler | policyHandler.Create |
| Display token | Refused — not a viewer-safe GET |
Request body
PolicyInput is what a caller may set on a policy. Narrow rather than binding store.Policy, which is what both handlers used to do. That accepted id, created_by, created_at and updated_at from the request body — so a caller could choose a policy's identifier and assert when it was written, and Create handed whatever arrived straight to the store. The binding tags carry the CHECK constraints that migration 001 put on this table. Without them an unknown rule_type reaches PostgreSQL and comes back as a raw SQLSTATE 23514, which tells an operator nothing about which of the values they should have sent. rule_type is deliberately narrower than the CHECK constraint. The column still permits approval_required, because rows using it may already exist and a migration that deleted somebody's policy would be worse than one that refuses new ones. Nothing can approve or reject a request in this build, so accepting a new policy that claims to gate on approval would be promising a control that does not exist. The engine reports any surviving row rather than passing it.
| Field | Type | Description | |
|---|---|---|---|
name | string | required | |
description | string | ||
is_enabled | boolean | IsEnabled is a pointer so that omitting it leaves the column's default of true, rather than a Go zero value quietly creating every policy disabled. | |
rule_type | string | required | one of key_size, key_type, ca_restriction, max_lifetime, naming |
rule_config | string | required | |
domain_pattern | string | ||
severity | string | one of INFO, WARNING, BLOCK |
Responses
| Status | Body |
|---|---|
201 | Created |
400 | { "error": … } |
500 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/policies' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "<name>",
"rule_type": "<rule_type>",
"rule_config": "<rule_config>",
"description": "<description>",
"is_enabled": false
}'GET /api/v1/policies/:id
| Who can call it | Any authenticated user |
| Handler | policyHandler.Get |
| Display token | Readable by an unattended screen |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
200 | A Policy object |
404 | { "error": … } |
500 | { "error": … } · could not read the policy |
Example request
curl -X GET 'https://certpilot.example.com/api/v1/policies/<id>' \
-H 'Authorization: Bearer <token>'PUT /api/v1/policies/:id
| Who can call it | Operator, admin |
| Handler | policyHandler.Update |
| Display token | Refused — not a viewer-safe GET |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Request body
PolicyInput is what a caller may set on a policy. Narrow rather than binding store.Policy, which is what both handlers used to do. That accepted id, created_by, created_at and updated_at from the request body — so a caller could choose a policy's identifier and assert when it was written, and Create handed whatever arrived straight to the store. The binding tags carry the CHECK constraints that migration 001 put on this table. Without them an unknown rule_type reaches PostgreSQL and comes back as a raw SQLSTATE 23514, which tells an operator nothing about which of the values they should have sent. rule_type is deliberately narrower than the CHECK constraint. The column still permits approval_required, because rows using it may already exist and a migration that deleted somebody's policy would be worse than one that refuses new ones. Nothing can approve or reject a request in this build, so accepting a new policy that claims to gate on approval would be promising a control that does not exist. The engine reports any surviving row rather than passing it.
| Field | Type | Description | |
|---|---|---|---|
name | string | required | |
description | string | ||
is_enabled | boolean | IsEnabled is a pointer so that omitting it leaves the column's default of true, rather than a Go zero value quietly creating every policy disabled. | |
rule_type | string | required | one of key_size, key_type, ca_restriction, max_lifetime, naming |
rule_config | string | required | |
domain_pattern | string | ||
severity | string | one of INFO, WARNING, BLOCK |
Responses
| Status | Body |
|---|---|
200 | Success |
400 | { "error": … } |
500 | { "error": … } |
Example request
curl -X PUT 'https://certpilot.example.com/api/v1/policies/<id>' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "<name>",
"rule_type": "<rule_type>",
"rule_config": "<rule_config>",
"description": "<description>",
"is_enabled": false
}'DELETE /api/v1/policies/:id
| Who can call it | Admin |
| Handler | policyHandler.Delete |
| Display token | Refused — not a viewer-safe GET |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
200 | An object with message (string) |
500 | { "error": … } |
Example request
curl -X DELETE 'https://certpilot.example.com/api/v1/policies/<id>' \
-H 'Authorization: Bearer <token>'