Skip to content

Policies ​

Generated from core/api/router.go. Edit the router, not this file.

MethodPathWho can call it
GET/api/v1/policiesAny authenticated user
POST/api/v1/policiesOperator, admin
GET/api/v1/policies/:idAny authenticated user
PUT/api/v1/policies/:idOperator, admin
DELETE/api/v1/policies/:idAdmin

Rule types, all of which are evaluated:

rule_typerule_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 itAny authenticated user
HandlerpolicyHandler.List
Display tokenReadable by an unattended screen

Responses ​

StatusBody
200An object with data (Policy[]), total
500{ "error": … }
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/policies' \
  -H 'Authorization: Bearer <token>'

POST /api/v1/policies ​

Who can call itOperator, admin
HandlerpolicyHandler.Create
Display tokenRefused — 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.

FieldTypeDescription
namestringrequired
descriptionstring
is_enabledbooleanIsEnabled 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_typestringrequiredone of key_size, key_type, ca_restriction, max_lifetime, naming
rule_configstringrequired
domain_patternstring
severitystringone of INFO, WARNING, BLOCK

Responses ​

StatusBody
201Created
400{ "error": … }
500{ "error": … }
Example request
bash
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 itAny authenticated user
HandlerpolicyHandler.Get
Display tokenReadable by an unattended screen

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
200A Policy object
404{ "error": … }
500{ "error": … }
· could not read the policy
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/policies/<id>' \
  -H 'Authorization: Bearer <token>'

PUT /api/v1/policies/:id ​

Who can call itOperator, admin
HandlerpolicyHandler.Update
Display tokenRefused — not a viewer-safe GET

Parameters ​

NameInDefault
idpathrequired

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.

FieldTypeDescription
namestringrequired
descriptionstring
is_enabledbooleanIsEnabled 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_typestringrequiredone of key_size, key_type, ca_restriction, max_lifetime, naming
rule_configstringrequired
domain_patternstring
severitystringone of INFO, WARNING, BLOCK

Responses ​

StatusBody
200Success
400{ "error": … }
500{ "error": … }
Example request
bash
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 itAdmin
HandlerpolicyHandler.Delete
Display tokenRefused — not a viewer-safe GET

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
200An object with message (string)
500{ "error": … }
Example request
bash
curl -X DELETE 'https://certpilot.example.com/api/v1/policies/<id>' \
  -H 'Authorization: Bearer <token>'