Admin API
REST API for server management, users, audit, EAB, and more
The Admin API provides a REST interface for managing the ACMEEH server. It is optional and must be explicitly enabled in the configuration. The API supports user management, audit logging, EAB credential management, identifier allowlists, CSR profiles, certificate operations, notification management, CRL rebuilds, and maintenance mode.
Admin Client
acmeeh-admin is the official Python client library and CLI for interacting with
the Admin API. Install it from PyPI:
pip install acmeeh-admin
The package provides:
Python library — programmatic access via
AcmeehAdminClientfor automation and scripting.CLI — the
acmeeh-admincommand-line tool for operators (user management, audit export, certificate search, EAB provisioning, maintenance mode, and more).
For full documentation and source code, see the acmeeh-admin repository.
Enabling the Admin API
Add the following to your configuration file to enable the Admin API:
admin_api:
enabled: true
base_path: /api
token_secret: ${ADMIN_TOKEN_SECRET}
token_expiry_seconds: 3600
Danger
Security
The token_secret is used to sign JWT bearer tokens. Use a strong, random value
(at least 32 characters) and keep it secret. The Admin API should be exposed only on
internal or management networks, never on the public internet.
Creating the Initial Admin User
Use the CLI to create the first admin user before starting the server:
PYTHONPATH=src python -m acmeeh -c config.yaml admin create-user \
--username admin --email admin@example.com --role admin
This prints a generated password to stdout. Store it securely — it cannot be retrieved
later. If lost, use the POST /api/me/reset-password endpoint or create a new user.
Authentication
All Admin API endpoints (except POST /api/auth/login) require a bearer token in the
Authorization header. Obtain a token by logging in, then include it in subsequent requests:
curl -H "Authorization: Bearer eyJ..." https://acme.example.com/api/users
Tokens expire after the configured token_expiry_seconds (default: 3600 seconds).
When a token expires, the server responds with 401 Unauthorized and you must log in again.
POST /api/auth/login
Authenticate with username and password to receive a bearer token. No authentication required. Rate limited per IP address and username to prevent brute-force attacks.
Request body:
{
"username": "admin",
"password": "generated-password"
}
Response 200:
{
"token": "eyJ...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"last_login_at": "2025-06-01T08:15:00Z"
}
}
Error 401: Invalid username or password.
Error 429: Rate limit exceeded. Retry after the interval specified in the Retry-After header.
POST /api/auth/logout
Revoke the current bearer token. Requires authentication.
Response 200:
{
"status": "logged_out"
}
Roles
The Admin API uses role-based access control. Each endpoint requires one or more roles:
Role |
Permissions |
|---|---|
|
Full access: create/delete users, manage EAB credentials, manage identifier allowlists, manage CSR profiles, bulk-revoke certificates, manage notifications, CRL rebuild, maintenance mode, audit log export |
|
Read-only access: list and view users, view audit logs, list and view certificates, list and view CSR profiles, view current user profile |
Pagination
List endpoints support two pagination strategies:
Cursor-based pagination (preferred)
Used by the audit log and certificate list endpoints. The server returns a Link header
with a rel="next" URL containing the cursor for the next page:
Link: </api/audit-log?cursor=eyJ...&limit=50>; rel="next"
Pass the cursor query parameter from the Link header to fetch the next page.
When there are no more results, the Link header is absent.
Offset-based pagination (fallback)
Used by some list endpoints. Pass limit and offset query parameters:
GET /api/certificates?limit=50&offset=100
Users
Manage admin panel users. Users have a role (admin or auditor) and can
be enabled or disabled. Passwords are server-generated and returned only at creation or reset time.
GET /api/users
List all admin users. admin, auditor
Response 200:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"last_login_at": "2025-06-01T08:15:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"username": "auditor1",
"email": "auditor@example.com",
"role": "auditor",
"enabled": true,
"created_at": "2025-02-01T14:00:00Z",
"updated_at": "2025-02-01T14:00:00Z",
"last_login_at": null
}
]
POST /api/users
Create a new admin user. A password is auto-generated and returned only in this response. admin
Request body:
{
"username": "operator",
"email": "op@example.com",
"role": "auditor"
}
Response 201:
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"username": "operator",
"email": "op@example.com",
"role": "auditor",
"enabled": true,
"created_at": "2025-06-15T09:00:00Z",
"updated_at": "2025-06-15T09:00:00Z",
"last_login_at": null,
"password": "a1B2c3D4e5F6g7H8"
}
Danger
Important
The password field is only included in the creation response. Store it
securely — it cannot be retrieved again. If lost, use
POST /api/me/reset-password to generate a new one.
Error 400: Missing username or email, or invalid role value.
GET /api/users/{user_id}
Get a specific user by ID. admin, auditor
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"last_login_at": "2025-06-01T08:15:00Z"
}
Error 404: User not found.
PATCH /api/users/{user_id}
Update a user. Supports partial updates — only include the fields you want to change. admin
Request body (partial update):
{
"enabled": false
}
Or change the role:
{
"role": "admin"
}
Or update multiple fields at once:
{
"enabled": true,
"role": "auditor"
}
Response 200:
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"username": "operator",
"email": "op@example.com",
"role": "auditor",
"enabled": false,
"created_at": "2025-06-15T09:00:00Z",
"updated_at": "2025-06-15T12:30:00Z",
"last_login_at": null
}
Error 404: User not found.
DELETE /api/users/{user_id}
Delete a user permanently. admin
Response: 204 No Content
Error 404: User not found.
GET /api/me
Get the current authenticated user’s profile. admin, auditor
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"last_login_at": "2025-06-01T08:15:00Z"
}
POST /api/me/reset-password
Reset your own password. Returns the user object with a new server-generated password. admin, auditor
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-06-15T12:45:00Z",
"last_login_at": "2025-06-01T08:15:00Z",
"password": "x9Y8w7V6u5T4s3R2"
}
Danger
Important
The password field is only included in this response. Store it securely.
Audit Log
The audit log records all administrative actions performed through the Admin API. Entries are immutable and cannot be modified or deleted.
GET /api/audit-log
Query the audit log with filtering and cursor-based pagination. admin, auditor
Query parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Filter by action type (e.g., |
|
UUID |
Filter by the user who performed the action |
|
ISO 8601 |
Return entries after this timestamp |
|
ISO 8601 |
Return entries before this timestamp |
|
string |
Pagination cursor from the |
|
integer |
Maximum number of entries to return (default: 50) |
Response 200:
[
{
"id": "aa0e8400-e29b-41d4-a716-446655440010",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"action": "user.create",
"target_user_id": "770e8400-e29b-41d4-a716-446655440002",
"details": {
"username": "operator",
"role": "auditor"
},
"ip_address": "10.0.1.50",
"created_at": "2025-06-15T09:00:00Z"
},
{
"id": "bb0e8400-e29b-41d4-a716-446655440011",
"user_id": null,
"action": "auth.login_failed",
"target_user_id": null,
"details": {
"username": "unknown"
},
"ip_address": "192.168.1.100",
"created_at": "2025-06-14T22:30:00Z"
}
]
Pagination: The Link header contains the URL for the next page when
more results are available:
Link: </api/audit-log?cursor=eyJ...&limit=50>; rel="next"
POST /api/audit-log/export
Export audit log entries as an NDJSON (newline-delimited JSON) stream. Supports the same filters as the list endpoint. admin
Request body (optional):
{
"action": "user.create",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"since": "2025-01-01T00:00:00Z",
"until": "2025-06-30T23:59:59Z"
}
All filter fields are optional. Omit the request body or send {} to export all entries.
Response 200: Content-Type: application/x-ndjson
{"id":"aa0e...","user_id":"550e...","action":"user.create","target_user_id":"770e...","details":{"username":"operator","role":"auditor"},"ip_address":"10.0.1.50","created_at":"2025-06-15T09:00:00Z"}
{"id":"bb0e...","user_id":null,"action":"auth.login_failed","target_user_id":null,"details":{"username":"unknown"},"ip_address":"192.168.1.100","created_at":"2025-06-14T22:30:00Z"}
External Account Binding (EAB)
When acme.eab_required is true, ACME clients must present a valid
EAB credential during account registration. Use these endpoints to create, list, and revoke EAB credentials.
GET /api/eab
List all EAB credentials. admin
Response 200:
[
{
"id": "cc0e8400-e29b-41d4-a716-446655440020",
"kid": "eab-key-001",
"label": "Team Alpha",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"used": true,
"used_at": "2025-06-16T11:00:00Z",
"revoked": false,
"created_at": "2025-06-15T10:00:00Z"
},
{
"id": "cc0e8400-e29b-41d4-a716-446655440021",
"kid": "eab-key-002",
"label": "Team Beta",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"account_id": null,
"used": false,
"used_at": null,
"revoked": false,
"created_at": "2025-06-15T10:05:00Z"
}
]
POST /api/eab
Create a new EAB credential. The hmac_key is only returned in this response.
admin
Request body:
{
"kid": "eab-key-003",
"label": "Team Gamma"
}
Response 201:
{
"id": "cc0e8400-e29b-41d4-a716-446655440022",
"kid": "eab-key-003",
"label": "Team Gamma",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"account_id": null,
"used": false,
"used_at": null,
"revoked": false,
"created_at": "2025-06-15T10:10:00Z",
"hmac_key": "dGhpc0lzQVNlY3JldEtleUZvckVBQg"
}
Danger
Important
The hmac_key is base64url-encoded and is only shown at creation time.
Provide the kid and hmac_key to the ACME client for use
during account registration with External Account Binding.
Error 400: Missing kid field.
GET /api/eab/{cred_id}
Get a specific EAB credential. The hmac_key is never included in this response.
admin
Response 200:
{
"id": "cc0e8400-e29b-41d4-a716-446655440020",
"kid": "eab-key-001",
"label": "Team Alpha",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"used": true,
"used_at": "2025-06-16T11:00:00Z",
"revoked": false,
"created_at": "2025-06-15T10:00:00Z"
}
Error 404: EAB credential not found.
POST /api/eab/{cred_id}/revoke
Revoke an EAB credential so it can no longer be used for account registration. Already-bound accounts are not affected. admin
Response 200:
{
"id": "cc0e8400-e29b-41d4-a716-446655440021",
"kid": "eab-key-002",
"label": "Team Beta",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"account_id": null,
"used": false,
"used_at": null,
"revoked": true,
"created_at": "2025-06-15T10:05:00Z"
}
Error 404: EAB credential not found.
EAB Allowed Identifiers
Associate allowed identifiers with EAB credentials to restrict which identifiers an account registered with a given EAB credential may request certificates for.
GET /api/eab/{cred_id}/allowed-identifiers
List allowed identifiers associated with an EAB credential. admin
Response 200:
[
{
"id": "ee0e8400-e29b-41d4-a716-446655440040",
"identifier_type": "dns",
"identifier_value": "*.example.com",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z"
}
]
Error 404: EAB credential not found.
PUT /api/eab/{cred_id}/allowed-identifiers/{identifier_id}
Associate an allowed identifier with an EAB credential. admin
Response: 204 No Content
Error 404: EAB credential or identifier not found.
DELETE /api/eab/{cred_id}/allowed-identifiers/{identifier_id}
Remove the association between an allowed identifier and an EAB credential. admin
Response: 204 No Content
Error 404: EAB credential or identifier not found.
EAB CSR Profile
Assign a CSR profile to an EAB credential. Accounts registered with this credential will have their certificate requests validated against the assigned profile.
GET /api/eab/{cred_id}/csr-profile
Get the CSR profile assigned to an EAB credential. Returns null if no
profile is assigned. admin
Response 200 (profile assigned):
{
"id": "ff0e8400-e29b-41d4-a716-446655440050",
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z"
}
Response 200 (no profile assigned):
null
Error 404: EAB credential not found.
PUT /api/eab/{cred_id}/csr-profile/{profile_id}
Assign a CSR profile to an EAB credential. admin
Response: 204 No Content
Error 404: EAB credential or CSR profile not found.
DELETE /api/eab/{cred_id}/csr-profile/{profile_id}
Remove the CSR profile assignment from an EAB credential. admin
Response: 204 No Content
Error 404: EAB credential or CSR profile not found.
Allowed Identifiers
Per-account identifier allowlists. When security.identifier_policy.enforce_account_allowlist
is enabled, accounts can only request certificates for identifiers explicitly assigned to them.
Identifiers support wildcards (e.g., *.example.com).
GET /api/allowed-identifiers
List all allowed identifier entries. admin
Response 200:
[
{
"id": "ee0e8400-e29b-41d4-a716-446655440040",
"identifier_type": "dns",
"identifier_value": "*.example.com",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"account_ids": [
"dd0e8400-e29b-41d4-a716-446655440030",
"dd0e8400-e29b-41d4-a716-446655440031"
]
},
{
"id": "ee0e8400-e29b-41d4-a716-446655440041",
"identifier_type": "dns",
"identifier_value": "internal.corp.local",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:05:00Z",
"account_ids": []
}
]
POST /api/allowed-identifiers
Create an allowed identifier entry. admin
Request body:
{
"type": "dns",
"value": "*.example.com"
}
Response 201:
{
"id": "ee0e8400-e29b-41d4-a716-446655440042",
"identifier_type": "dns",
"identifier_value": "*.example.com",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:15:00Z",
"account_ids": []
}
Error 400: Missing type or value field.
GET /api/allowed-identifiers/{id}
Get a specific allowed identifier entry, including associated account IDs. admin
Response 200:
{
"id": "ee0e8400-e29b-41d4-a716-446655440040",
"identifier_type": "dns",
"identifier_value": "*.example.com",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"account_ids": [
"dd0e8400-e29b-41d4-a716-446655440030",
"dd0e8400-e29b-41d4-a716-446655440031"
]
}
Error 404: Identifier not found.
DELETE /api/allowed-identifiers/{id}
Delete an allowed identifier entry. This cascades to remove all account associations for this identifier. admin
Response: 204 No Content
Error 404: Identifier not found.
PUT /api/allowed-identifiers/{identifier_id}/accounts/{account_id}
Associate an allowed identifier with an ACME account. After this association, the account is permitted to request certificates for this identifier. admin
Response: 204 No Content
Error 404: Identifier or account not found.
DELETE /api/allowed-identifiers/{identifier_id}/accounts/{account_id}
Remove the association between an allowed identifier and an ACME account. The account will no longer be permitted to request certificates for this identifier. admin
Response: 204 No Content
Error 404: Identifier or account not found.
GET /api/accounts/{account_id}/allowed-identifiers
List all allowed identifiers for a specific ACME account. admin
Response 200:
[
{
"id": "ee0e8400-e29b-41d4-a716-446655440040",
"identifier_type": "dns",
"identifier_value": "*.example.com",
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"account_ids": [
"dd0e8400-e29b-41d4-a716-446655440030"
]
}
]
Error 404: Account not found.
CSR Profiles
Manage certificate profiles that control key usages, extended key usages, validity period, and
other certificate properties. Profiles can be assigned to ACME accounts to enforce certificate
policies per account. Set security.require_csr_profile: true to reject certificate
finalization for accounts without an assigned profile (see CSR Profile Enforcement).
GET /api/csr-profiles
List all CSR profiles. admin, auditor
Response 200:
[
{
"id": "ff0e8400-e29b-41d4-a716-446655440050",
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z"
},
{
"id": "ff0e8400-e29b-41d4-a716-446655440051",
"name": "client-auth",
"description": "Client authentication profile",
"profile_data": {
"key_usages": ["digital_signature"],
"extended_key_usages": ["client_auth"],
"validity_days": 365
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:05:00Z",
"updated_at": "2025-06-15T10:05:00Z"
}
]
POST /api/csr-profiles
Create a new CSR profile. admin
Request body:
{
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
}
}
Response 201:
{
"id": "ff0e8400-e29b-41d4-a716-446655440052",
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:20:00Z",
"updated_at": "2025-06-15T10:20:00Z"
}
Error 400: Missing name or profile_data field.
GET /api/csr-profiles/{profile_id}
Get a specific CSR profile, including the list of associated account IDs. admin, auditor
Response 200:
{
"id": "ff0e8400-e29b-41d4-a716-446655440050",
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z",
"account_ids": [
"dd0e8400-e29b-41d4-a716-446655440030"
]
}
Error 404: CSR profile not found.
PUT /api/csr-profiles/{profile_id}
Update a CSR profile. This is a full replacement — all fields must be provided. admin
Request body:
{
"name": "server-tls-updated",
"description": "Updated server TLS profile with longer validity",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 180
}
}
Response 200:
{
"id": "ff0e8400-e29b-41d4-a716-446655440050",
"name": "server-tls-updated",
"description": "Updated server TLS profile with longer validity",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 180
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T14:00:00Z"
}
Error 400: Missing name or profile_data field.
Error 404: CSR profile not found.
DELETE /api/csr-profiles/{profile_id}
Delete a CSR profile. admin
Response: 204 No Content
Error 404: CSR profile not found.
POST /api/csr-profiles/{profile_id}/validate
Dry-run validate a CSR against a profile without issuing a certificate. Useful for testing that a CSR conforms to a profile’s constraints before submitting an order. admin, auditor
Request body:
{
"csr": "MIICYzCCAUsCAQAwHj...base64-DER-encoded-CSR"
}
Response 200: Validation result object describing whether the CSR passes the profile’s requirements, and any violations found.
Error 404: CSR profile not found.
PUT /api/csr-profiles/{profile_id}/accounts/{account_id}
Assign a CSR profile to an ACME account. The account’s certificate requests will be validated against this profile. admin
Response: 204 No Content
Error 404: CSR profile or account not found.
DELETE /api/csr-profiles/{profile_id}/accounts/{account_id}
Remove a CSR profile assignment from an ACME account. The account will revert to the default certificate policy. admin
Response: 204 No Content
Error 404: CSR profile or account not found.
GET /api/accounts/{account_id}/csr-profile
Get the CSR profile assigned to a specific ACME account. Returns null if no
profile is assigned. admin, auditor
Response 200 (profile assigned):
{
"id": "ff0e8400-e29b-41d4-a716-446655440050",
"name": "server-tls",
"description": "Standard server TLS profile",
"profile_data": {
"key_usages": ["digital_signature", "key_encipherment"],
"extended_key_usages": ["server_auth"],
"validity_days": 90
},
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-06-15T10:00:00Z",
"updated_at": "2025-06-15T10:00:00Z"
}
Response 200 (no profile assigned):
null
Error 404: Account not found.
CSR Profile Reference
The profile_data object controls how CSRs are validated against the profile. All fields
are optional — omitted fields disable that particular check. Here is a complete example with every
supported field:
{
"authorized_keys": {
"RSA": 2048,
"EC.secp256r1": 256,
"EC.secp384r1": 384,
"EC.secp521r1": 521,
"Ed25519": 0,
"Ed448": 0
},
"authorized_signature_algorithms": [
"SHA256withRSA",
"SHA384withRSA",
"SHA512withRSA",
"SHA256withECDSA",
"SHA384withECDSA",
"SHA512withECDSA",
"Ed25519",
"Ed448"
],
"authorized_key_usages": [
"digital_signature",
"content_commitment",
"key_encipherment",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
"encipher_only",
"decipher_only"
],
"authorized_extended_key_usages": [
"serverAuth",
"clientAuth",
"codeSigning",
"emailProtection",
"timeStamping",
"OCSPSigning"
],
"common_name_minimum": 0,
"common_name_maximum": -1,
"common_name_regex": ".*",
"san_minimum": -1,
"san_maximum": -1,
"san_regex": ".*",
"san_types": [
"DNS_NAME",
"IP_ADDRESS",
"RFC822_NAME",
"URI"
],
"subject_regex": ".*",
"wildcard_in_common_name": true,
"wildcard_in_san": true,
"max_subdomain_depth": 10,
"depth_base_domains": ["corp.internal"],
"reuse_key": true,
"renewal_window_days": 0
}
Field Reference
Field |
Type |
Description |
|---|---|---|
|
object |
Map of key type label to minimum key size in bits. Use |
|
array of strings |
Allowed CSR signature algorithms. Values are case-sensitive and must match exactly. |
|
array of strings |
Allowed Key Usage extension values. If the CSR includes a Key Usage extension with values not in this list, validation fails. |
|
array of strings |
Allowed Extended Key Usage values. If the CSR includes an EKU extension with values not in this list, validation fails. |
|
integer |
Minimum number of CN attributes required. |
|
integer |
Maximum number of CN attributes allowed. |
|
string |
Python regex that each CN value must fully match ( |
|
integer |
Minimum number of SANs required. |
|
integer |
Maximum number of SANs allowed. |
|
string |
Python regex that each SAN value must fully match. |
|
array of strings |
Allowed SAN types. CSRs containing a SAN type not in this list are rejected. |
|
string |
Python regex matched against the CSR subject in RFC 4514 format
(e.g., |
|
boolean |
|
|
boolean |
|
|
integer |
Maximum number of labels beyond a base domain. Requires |
|
array of strings |
Base domains for subdomain depth checking. Both this and |
|
boolean |
|
|
integer |
When positive, blocks issuance if an active certificate for the same hosts does not
expire within this many days. |
Allowed Values
SAN Types (san_types)
Value |
Description |
|---|---|
|
DNS domain name |
|
IPv4 or IPv6 address |
|
Email address |
|
Uniform Resource Identifier |
Example Profiles
Web Server TLS
{
"authorized_keys": { "RSA": 2048, "EC.secp256r1": 256, "EC.secp384r1": 384 },
"authorized_signature_algorithms": ["SHA256withRSA", "SHA384withRSA", "SHA256withECDSA", "SHA384withECDSA"],
"authorized_key_usages": ["digital_signature", "key_encipherment"],
"authorized_extended_key_usages": ["serverAuth"],
"common_name_minimum": 1,
"common_name_maximum": 1,
"common_name_regex": "^[a-z0-9.-]+\\.corp\\.internal$",
"san_minimum": 1,
"san_maximum": 10,
"san_regex": "^[a-z0-9.-]+\\.corp\\.internal$",
"san_types": ["DNS_NAME"],
"wildcard_in_common_name": false,
"wildcard_in_san": false,
"max_subdomain_depth": 2,
"depth_base_domains": ["corp.internal"],
"reuse_key": false,
"renewal_window_days": 30
}
Client Authentication
{
"authorized_keys": { "RSA": 2048, "EC.secp256r1": 256 },
"authorized_signature_algorithms": ["SHA256withRSA", "SHA256withECDSA"],
"authorized_key_usages": ["digital_signature"],
"authorized_extended_key_usages": ["clientAuth"],
"san_minimum": 1,
"san_maximum": 1,
"san_types": ["RFC822_NAME"],
"wildcard_in_common_name": false,
"reuse_key": false
}
Code Signing
{
"authorized_keys": { "RSA": 4096, "EC.secp384r1": 384 },
"authorized_signature_algorithms": ["SHA384withRSA", "SHA512withRSA", "SHA384withECDSA"],
"authorized_key_usages": ["digital_signature"],
"authorized_extended_key_usages": ["codeSigning"],
"common_name_minimum": 1,
"common_name_maximum": 1,
"reuse_key": false
}
Certificates
Search, inspect, and bulk-revoke certificates issued by the ACME server.
GET /api/certificates
Search and list certificates with filtering and pagination. admin, auditor
Query parameters:
Parameter |
Type |
Description |
|---|---|---|
|
UUID |
Filter by ACME account |
|
string |
Filter by serial number (exact match) |
|
string |
Filter by SHA-256 fingerprint (hex) |
|
string |
Filter by status ( |
|
string |
Filter by SAN domain value |
|
ISO 8601 |
Find certificates expiring before this date |
|
integer |
Maximum results to return (default: 50) |
|
integer |
Offset for pagination |
Response 200:
[
{
"id": "110e8400-e29b-41d4-a716-446655440060",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"order_id": "220e8400-e29b-41d4-a716-446655440070",
"serial_number": "01A2B3C4D5E6F7",
"fingerprint": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"not_before": "2025-06-15T10:00:00Z",
"not_after": "2025-09-13T10:00:00Z",
"revoked_at": null,
"revocation_reason": null,
"san_values": ["example.com", "www.example.com"],
"created_at": "2025-06-15T10:00:00Z"
},
{
"id": "110e8400-e29b-41d4-a716-446655440061",
"account_id": "dd0e8400-e29b-41d4-a716-446655440031",
"order_id": "220e8400-e29b-41d4-a716-446655440071",
"serial_number": "02B3C4D5E6F7A8",
"fingerprint": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
"not_before": "2025-05-01T08:00:00Z",
"not_after": "2025-07-30T08:00:00Z",
"revoked_at": "2025-06-10T16:30:00Z",
"revocation_reason": "keyCompromise",
"san_values": ["api.internal.corp"],
"created_at": "2025-05-01T08:00:00Z"
}
]
Pagination: Uses Link header with offset-based pagination:
Link: </api/certificates?limit=50&offset=50>; rel="next"
GET /api/certificates/{serial}
Get a specific certificate by serial number. admin, auditor
Response 200:
{
"id": "110e8400-e29b-41d4-a716-446655440060",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"order_id": "220e8400-e29b-41d4-a716-446655440070",
"serial_number": "01A2B3C4D5E6F7",
"fingerprint": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"not_before": "2025-06-15T10:00:00Z",
"not_after": "2025-09-13T10:00:00Z",
"revoked_at": null,
"revocation_reason": null,
"san_values": ["example.com", "www.example.com"],
"created_at": "2025-06-15T10:00:00Z"
}
Error 404: Certificate not found.
GET /api/certificates/by-fingerprint/{fingerprint}
Look up a certificate by its SHA-256 fingerprint (hex-encoded). admin, auditor
Response 200:
{
"id": "110e8400-e29b-41d4-a716-446655440060",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"order_id": "220e8400-e29b-41d4-a716-446655440070",
"serial_number": "01A2B3C4D5E6F7",
"fingerprint": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"not_before": "2025-06-15T10:00:00Z",
"not_after": "2025-09-13T10:00:00Z",
"revoked_at": null,
"revocation_reason": null,
"san_values": ["example.com", "www.example.com"],
"created_at": "2025-06-15T10:00:00Z"
}
Error 404: No certificate found with the given fingerprint.
POST /api/certificates/bulk-revoke
Revoke multiple certificates at once using flexible filter criteria. Supports a dry-run mode to preview which certificates would be affected before committing. admin
Request body:
{
"filter": {
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"serial_numbers": ["01A2B3C4D5E6F7", "02B3C4D5E6F7A8"],
"domain": "example.com",
"issued_before": "2025-06-01T00:00:00Z",
"issued_after": "2025-01-01T00:00:00Z"
},
"reason": 4,
"dry_run": false
}
All filter fields are optional and can be combined. At least one filter field must be provided.
The reason field uses RFC 5280 revocation reason codes:
Code |
Reason |
|---|---|
0 |
unspecified |
1 |
keyCompromise |
2 |
cACompromise |
3 |
affiliationChanged |
4 |
superseded |
5 |
cessationOfOperation |
9 |
privilegeWithdrawn |
Response 200 (dry_run=true):
{
"dry_run": true,
"matching_certificates": 5,
"serial_numbers": [
"01A2B3C4D5E6F7",
"02B3C4D5E6F7A8",
"03C4D5E6F7A8B9",
"04D5E6F7A8B9CA",
"05E6F7A8B9CADB"
]
}
Response 200 (dry_run=false):
{
"revoked": 5,
"errors": [],
"total_matched": 5
}
If some certificates fail to revoke, they are listed in the errors array:
{
"revoked": 3,
"errors": [
{"serial_number": "04D5E6F7A8B9CA", "error": "already revoked"},
{"serial_number": "05E6F7A8B9CADB", "error": "CA backend error"}
],
"total_matched": 5
}
Error 400: Missing filter field, empty filter object, or invalid reason code.
Notifications
View and manage notification delivery. Notifications are sent for events such as certificate expiration warnings, revocations, and other administrative events.
GET /api/notifications
List notifications with filtering and pagination. admin
Query parameters:
Parameter |
Type |
Description |
|---|---|---|
|
string |
Filter by status ( |
|
integer |
Maximum results to return (default: 50) |
|
integer |
Offset for pagination |
Response 200:
[
{
"id": "330e8400-e29b-41d4-a716-446655440080",
"notification_type": "expiration_warning",
"recipient": "admin@example.com",
"subject": "Certificate expiring: example.com",
"status": "sent",
"account_id": "dd0e8400-e29b-41d4-a716-446655440030",
"error_detail": null,
"retry_count": 0,
"created_at": "2025-06-15T06:00:00Z",
"sent_at": "2025-06-15T06:00:05Z"
},
{
"id": "330e8400-e29b-41d4-a716-446655440081",
"notification_type": "revocation_succeeded",
"recipient": "ops@example.com",
"subject": "Certificate revoked: api.internal.corp",
"status": "failed",
"account_id": "dd0e8400-e29b-41d4-a716-446655440031",
"error_detail": "SMTP connection timeout",
"retry_count": 3,
"created_at": "2025-06-10T16:30:00Z",
"sent_at": null
}
]
POST /api/notifications/retry
Retry all failed notifications. Resets the retry count and re-queues failed notifications for delivery. admin
Response 200:
{
"retried": 5
}
POST /api/notifications/purge
Purge old notifications older than the specified number of days. admin
Request body (optional):
{
"days": 30
}
If omitted, defaults to 30 days.
Response 200:
{
"purged": 100
}
Error 400: days must be at least 1.
CRL Management
Manage the Certificate Revocation List. Requires crl.enabled: true in the server
configuration.
POST /api/crl/rebuild
Force an immediate CRL rebuild. Returns the current CRL health status after rebuild. admin
Response 200: CRL health status object with details about the rebuilt CRL.
Error 503: CRL subsystem is not enabled in the server configuration.
Maintenance Mode
Toggle maintenance mode for the ACME server. When maintenance mode is enabled, new order
and pre-authorization requests return 503 Service Unavailable with a Retry-After
header. In-progress operations (order finalization, challenge validation, certificate
downloads, account operations) are allowed to complete. The Admin API remains accessible
during maintenance.
GET /api/maintenance
Get the current maintenance mode status. admin
Response 200:
{
"maintenance_mode": false
}
POST /api/maintenance
Enable or disable maintenance mode. When enabled, new order and pre-authorization
requests return 503 Service Unavailable to clients while in-progress operations
(order finalization, challenge validation, certificate downloads) are allowed to complete.
The Admin API remains fully accessible. admin
Request body:
{
"enabled": true
}
Response 200:
{
"maintenance_mode": true
}
To disable maintenance mode:
{
"enabled": false
}
Response 200:
{
"maintenance_mode": false
}
Error 400: Missing enabled field.
Error Responses
Admin API errors are returned as JSON objects with an appropriate HTTP status code:
{
"error": "Unauthorized",
"message": "Invalid or expired token"
}
Common error status codes:
Status |
Meaning |
|---|---|
|
Bad Request — missing or invalid fields in the request body |
|
Unauthorized — missing, invalid, or expired bearer token |
|
Forbidden — authenticated but insufficient role for this endpoint |
|
Not Found — requested resource does not exist |
|
Too Many Requests — rate limit exceeded (login endpoint) |
|
Service Unavailable — required subsystem not enabled (e.g., CRL) |