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 AcmeehAdminClient for automation and scripting.

  • CLI — the acmeeh-admin command-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

admin

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

auditor

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

action

string

Filter by action type (e.g., user.create, eab.revoke)

user_id

UUID

Filter by the user who performed the action

since

ISO 8601

Return entries after this timestamp

until

ISO 8601

Return entries before this timestamp

cursor

string

Pagination cursor from the Link header

limit

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

authorized_keys

object

Map of key type label to minimum key size in bits. Use 0 for fixed-size algorithms (Ed25519, Ed448). CSRs with a key type not in this map are rejected.

authorized_signature_algorithms

array of strings

Allowed CSR signature algorithms. Values are case-sensitive and must match exactly.

authorized_key_usages

array of strings

Allowed Key Usage extension values. If the CSR includes a Key Usage extension with values not in this list, validation fails.

authorized_extended_key_usages

array of strings

Allowed Extended Key Usage values. If the CSR includes an EKU extension with values not in this list, validation fails.

common_name_minimum

integer

Minimum number of CN attributes required. -1 = no minimum (default).

common_name_maximum

integer

Maximum number of CN attributes allowed. -1 = no maximum (default).

common_name_regex

string

Python regex that each CN value must fully match (fullmatch).

san_minimum

integer

Minimum number of SANs required. -1 = no minimum (default).

san_maximum

integer

Maximum number of SANs allowed. -1 = no maximum (default).

san_regex

string

Python regex that each SAN value must fully match.

san_types

array of strings

Allowed SAN types. CSRs containing a SAN type not in this list are rejected.

subject_regex

string

Python regex matched against the CSR subject in RFC 4514 format (e.g., CN=example.com,O=Corp,C=US).

wildcard_in_common_name

boolean

false rejects CN values starting with *.. Omit or set true to allow wildcards.

wildcard_in_san

boolean

false rejects DNS SAN values starting with *.. Omit or set true to allow wildcards.

max_subdomain_depth

integer

Maximum number of labels beyond a base domain. Requires depth_base_domains. Example: depth 2 with base corp.internal allows a.b.corp.internal but not a.b.c.corp.internal.

depth_base_domains

array of strings

Base domains for subdomain depth checking. Both this and max_subdomain_depth must be present for the check to activate.

reuse_key

boolean

false rejects CSRs whose public key matches a previously-issued certificate. Omit or set true to allow key reuse.

renewal_window_days

integer

When positive, blocks issuance if an active certificate for the same hosts does not expire within this many days. 0 disables the check (default).

Allowed Values

Key Type Labels (authorized_keys)

Key type labels must match exactly what the server derives from the CSR public key:

Label

Key Type

Size Constraint

RSA

RSA

Minimum bits (e.g., 2048)

EC.secp256r1

ECDSA P-256

256

EC.secp384r1

ECDSA P-384

384

EC.secp521r1

ECDSA P-521

521

Ed25519

EdDSA (Curve25519)

Fixed — use 0

Ed448

EdDSA (Curve448)

Fixed — use 0

The EC. prefix followed by the curve name is required for elliptic curve keys. The curve name comes from Python’s cryptography library (e.g., secp256r1, not prime256v1).

Signature Algorithms (authorized_signature_algorithms)

These are the only recognized values (case-sensitive):

Value

Algorithm

SHA256withRSA

RSA with SHA-256

SHA384withRSA

RSA with SHA-384

SHA512withRSA

RSA with SHA-512

SHA256withECDSA

ECDSA with SHA-256

SHA384withECDSA

ECDSA with SHA-384

SHA512withECDSA

ECDSA with SHA-512

Ed25519

EdDSA Curve25519

Ed448

EdDSA Curve448

Danger

Important

Algorithm names are case-sensitive and use a specific format (e.g., SHA256withRSA, not sha256WithRSAEncryption). Algorithms not listed above (DSA, MD5, SHA-1, GOST, RSASSA-PSS) are not supported.

Key Usages (authorized_key_usages)

Values use snake_case matching Python’s cryptography.x509.KeyUsage attributes:

Value

Description

digital_signature

Verify digital signatures (TLS, authentication)

content_commitment

Non-repudiation

key_encipherment

Encrypt keys (RSA key exchange)

data_encipherment

Encrypt data directly

key_agreement

Key agreement (ECDH)

key_cert_sign

Sign certificates (CA only)

crl_sign

Sign CRLs (CA only)

encipher_only

Encipher during key agreement

decipher_only

Decipher during key agreement

Extended Key Usages (authorized_extended_key_usages)

Values use camelCase names. Only these six OIDs are mapped by name:

Value

OID

Description

serverAuth

1.3.6.1.5.5.7.3.1

TLS server authentication

clientAuth

1.3.6.1.5.5.7.3.2

TLS client authentication

codeSigning

1.3.6.1.5.5.7.3.3

Code signing

emailProtection

1.3.6.1.5.5.7.3.4

S/MIME email

timeStamping

1.3.6.1.5.5.7.3.8

Trusted timestamping

OCSPSigning

1.3.6.1.5.5.7.3.9

OCSP response signing

For EKU OIDs not listed above, the dotted-string OID itself (e.g., 1.3.6.1.5.5.7.3.17) is used as the comparison value.

SAN Types (san_types)

Value

Description

DNS_NAME

DNS domain name

IP_ADDRESS

IPv4 or IPv6 address

RFC822_NAME

Email address

URI

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

account_id

UUID

Filter by ACME account

serial

string

Filter by serial number (exact match)

fingerprint

string

Filter by SHA-256 fingerprint (hex)

status

string

Filter by status (active, revoked, expired)

domain

string

Filter by SAN domain value

expiring_before

ISO 8601

Find certificates expiring before this date

limit

integer

Maximum results to return (default: 50)

offset

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

status

string

Filter by status (pending, sent, failed)

limit

integer

Maximum results to return (default: 50)

offset

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

400

Bad Request — missing or invalid fields in the request body

401

Unauthorized — missing, invalid, or expired bearer token

403

Forbidden — authenticated but insufficient role for this endpoint

404

Not Found — requested resource does not exist

429

Too Many Requests — rate limit exceeded (login endpoint)

503

Service Unavailable — required subsystem not enabled (e.g., CRL)