CertMate Client Certificates - API Reference

September 9, 2026 · View on GitHub

Overview

The CertMate Client Certificates API provides REST endpoints for complete certificate management with authentication, rate limiting, and audit logging.

Base URL: http://localhost:8000

Every path below is absolute and starts with /api. It used to be a mix: thirteen paths written relative to a base ending in /api, and eight written with the prefix — so against the stated base one set resolved to /api/api/.... The four translations had lost the base-URL line entirely, which left their relative paths with nothing to resolve against at all. Authentication: Bearer Token (required on all endpoints) Content-Type: application/json


Authentication

All API endpoints require Bearer token authentication.

Header Format

Authorization: Bearer YOUR_TOKEN

Example Request

curl -X GET http://localhost:8000/api/client-certs \
 -H "Authorization: Bearer YOUR_TOKEN" \
 -H "Content-Type: application/json"

Rate Limiting

API endpoints have rate limits to prevent abuse:

EndpointLimitPer
General100minute
Create Certificate30minute
Batch Operations10minute
OCSP Status200minute
CRL Download60minute
Per-IP ceiling600minute

The working bucket is per API key (requests authenticated with the same bearer key share one limit) and per IP for session/anonymous requests, so several clients behind one NAT or proxy do not share — and abuse — a single bucket.

Every /api/ request is also counted against a coarse per-IP ceiling, checked first. It sits far above the working limits, so a normal client never meets it; it exists because a bucket keyed only on the caller-supplied bearer token can be reset at will by changing the token, which previously made the per-key limits — and the protection on the unauthenticated OCSP and CRL endpoints — bypassable.

Configuring rate limits

The limits are configurable per instance (admin only), so a trusted automation fleet behind a single address can raise them instead of tripping the default. Settings → API Keys → API Rate Limits exposes a value-per-endpoint form and an on/off toggle; changes apply immediately, with no restart.

The same configuration is available over the API:

GET /api/settings/rate-limits
  -> { "enabled": true,
       "limits": { "default": 100, "certificate_create": 30, ... },
       "defaults": { ... } }

PUT /api/settings/rate-limits
  { "enabled": true, "limits": { "certificate_create": 500 } }

Each limit is requests per minute (1–100000). Only the endpoint keys returned by GET are accepted; omitted keys keep their default. Setting "enabled": false turns API rate limiting off entirely (the login endpoint keeps its own separate limiter regardless).

Rate Limit Response

When rate limited, you'll receive:

HTTP 429 Too Many Requests

{
 "error": "Rate limit exceeded",
 "message": "Too many requests. Please try again later.",
 "retry_after": 60
}

Endpoints

Certificate Management

1. Create Certificate

Endpoint: POST /api/client-certs/create

Create a new client certificate.

Request:

{
 "common_name": "user@example.com",
 "email": "user@example.com",
 "organization": "ACME Corp",
 "organizational_unit": "Engineering",
 "cert_usage": "api-mtls",
 "days_valid": 365,
 "generate_key": true,
 "notes": "Production certificate"
}

Parameters:

  • common_name (required) - Certificate subject
  • email (optional) - Email address
  • organization (optional) - Organization name
  • organizational_unit (optional) - Department name
  • cert_usage (optional) - Usage type: api-mtls, vpn, or custom
  • days_valid (optional) - Validity in days (default: 365)
  • generate_key (optional) - Generate private key (default: true)
  • notes (optional) - Additional notes

Response (201 Created):

{
 "identifier": "cert-abc123",
 "common_name": "user@example.com",
 "serial_number": "12345678901234567890",
 "created_at": "2024-10-30T18:00:00Z",
 "expires_at": "2025-10-30T18:00:00Z",
 "cert_usage": "api-mtls",
 "status": "active"
}

Example:

curl -X POST http://localhost:8000/api/client-certs/create \
 -H "Authorization: Bearer TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "common_name": "user@example.com",
 "email": "user@example.com",
 "organization": "ACME Corp",
 "cert_usage": "api-mtls",
 "days_valid": 365
 }'

2. List Certificates

Endpoint: GET /api/client-certs

List all client certificates with optional filtering.

Query Parameters:

  • usage (optional) - Filter by usage type (e.g., api-mtls)
  • revoked (optional) - Filter by status (true or false)
  • search (optional) - Search in common name

Response (200 OK):

{
 "certificates": [{
 "identifier": "cert-001",
 "common_name": "user1@example.com",
 "organization": "ACME Corp",
 "cert_usage": "api-mtls",
 "created_at": "2024-10-30T18:00:00Z",
 "expires_at": "2025-10-30T18:00:00Z",
 "revoked": false,
 "status": "active"
 },
 {
 "identifier": "cert-002",
 "common_name": "user2@example.com",
 "organization": "ACME Corp",
 "cert_usage": "vpn",
 "created_at": "2024-10-29T18:00:00Z",
 "expires_at": "2025-10-29T18:00:00Z",
 "revoked": true,
 "status": "revoked"
 }
 ],
 "total": 2
}

Examples:

# List all certificates
curl http://localhost:8000/api/client-certs \
 -H "Authorization: Bearer TOKEN"

# Filter by usage type
curl "http://localhost:8000/api/client-certs?usage=api-mtls" \
 -H "Authorization: Bearer TOKEN"

# List only revoked
curl "http://localhost:8000/api/client-certs?revoked=true" \
 -H "Authorization: Bearer TOKEN"

# Search by common name
curl "http://localhost:8000/api/client-certs?search=user1" \
 -H "Authorization: Bearer TOKEN"

3. Get Certificate Details

Endpoint: GET /api/client-certs/<identifier>

Get complete metadata for a certificate.

Response (200 OK):

{
 "type": "client_certificate",
 "identifier": "cert-001",
 "common_name": "user@example.com",
 "email": "user@example.com",
 "organization": "ACME Corp",
 "organizational_unit": "Engineering",
 "serial_number": "12345678901234567890",
 "created_at": "2024-10-30T18:00:00Z",
 "expires_at": "2025-10-30T18:00:00Z",
 "cert_usage": "api-mtls",
 "notes": "Production certificate",
 "revocation": {
 "revoked": false,
 "revoked_at": null,
 "reason_revoked": null
 },
 "renewal": {
 "renewal_enabled": true,
 "renewal_threshold_days": 30
 }
}

Example:

curl http://localhost:8000/api/client-certs/cert-001 \
 -H "Authorization: Bearer TOKEN"

4. Download Certificate Files

Endpoint: GET /api/client-certs/<identifier>/download/<type>

Download certificate, private key, or CSR file.

Parameters:

  • identifier - Certificate ID
  • type - File type: crt, key, csr, or pfx (encrypted PKCS#12; requires a PFX password set in Settings, operator role)

Response (200 OK):

  • Content-Type: application/octet-stream
  • File attachment with proper naming

Examples:

# Download certificate
curl http://localhost:8000/api/client-certs/cert-001/download/crt \
 -H "Authorization: Bearer TOKEN" \
 -o certificate.crt

# Download private key
curl http://localhost:8000/api/client-certs/cert-001/download/key \
 -H "Authorization: Bearer TOKEN" \
 -o private.key

# Download CSR
curl http://localhost:8000/api/client-certs/cert-001/download/csr \
 -H "Authorization: Bearer TOKEN" \
 -o request.csr

5. Revoke Certificate

Endpoint: POST /api/client-certs/<identifier>/revoke

Revoke a certificate with optional reason.

Request (optional):

{
 "reason": "compromised"
}

Response (200 OK):

{
 "message": "Certificate revoked: cert-001",
 "revoked_at": "2024-10-30T18:15:00Z",
 "reason": "compromised"
}

Example:

curl -X POST http://localhost:8000/api/client-certs/cert-001/revoke \
 -H "Authorization: Bearer TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "reason": "compromised"
 }'

6. Renew Certificate

Endpoint: POST /api/client-certs/<identifier>/renew

Renew a certificate (same CN, new serial).

Response (201 Created):

{
 "identifier": "cert-001-renewed",
 "common_name": "user@example.com",
 "serial_number": "98765432109876543210",
 "created_at": "2024-10-30T18:20:00Z",
 "expires_at": "2025-10-30T18:20:00Z",
 "status": "active"
}

Example:

curl -X POST http://localhost:8000/api/client-certs/cert-001/renew \
 -H "Authorization: Bearer TOKEN"

7. Get Statistics

Endpoint: GET /api/client-certs/stats

Get certificate usage statistics.

Response (200 OK):

{
 "total": 100,
 "active": 85,
 "revoked": 15,
 "expiring_soon": 8,
 "by_usage": {
 "api-mtls": 60,
 "vpn": 35,
 "other": 5
 },
 "created_count": 100,
 "renewal_enabled": 92
}

Example:

curl http://localhost:8000/api/client-certs/stats \
 -H "Authorization: Bearer TOKEN"

8. Batch Import Certificates

Endpoint: POST /api/client-certs/batch

Create multiple certificates from CSV data in single request.

Request:

{
 "headers": ["common_name", "email", "organization", "cert_usage", "days_valid"],
 "rows": [["user1@example.com", "user1@example.com", "ACME Corp", "api-mtls", "365"],
 ["user2@example.com", "user2@example.com", "ACME Corp", "vpn", "365"],
 ["user3@example.com", "user3@example.com", "ACME Corp", "api-mtls", "365"]
 ]
}

Response (201 Created):

{
 "total": 3,
 "successful": 3,
 "failed": 0,
 "errors": [],
 "certificates": [{
 "identifier": "cert-batch-001",
 "common_name": "user1@example.com"
 },
 {
 "identifier": "cert-batch-002",
 "common_name": "user2@example.com"
 },
 {
 "identifier": "cert-batch-003",
 "common_name": "user3@example.com"
 }
 ]
}

Example:

curl -X POST http://localhost:8000/api/client-certs/batch \
 -H "Authorization: Bearer TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "headers": ["common_name", "email", "organization"],
 "rows": [["user1@example.com", "user1@example.com", "ACME Corp"],
 ["user2@example.com", "user2@example.com", "ACME Corp"]
 ]
 }'

OCSP & CRL

9. OCSP Status Query

Endpoint: GET /api/ocsp/status/<serial_number>

Query certificate status via OCSP.

Response (200 OK):

{
 "response_status": "successful",
 "certificate_status": "good|revoked|unknown",
 "certificate_serial": 12345678,
 "this_update": "2024-10-30T18:00:00Z",
 "next_update": null,
 "responder_name": "CertMate OCSP Responder"
}

Example:

curl http://localhost:8000/api/ocsp/status/12345678 \
 -H "Authorization: Bearer TOKEN"

10. CRL Distribution

Endpoint: GET /api/crl/download/<format_type>

Download Certificate Revocation List.

Parameters:

  • format_type - pem, der, or info

Response:

  • For pem and der: File attachment
  • For info: JSON with CRL metadata

Examples:

# Download CRL in PEM format
curl http://localhost:8000/api/crl/download/pem \
 -H "Authorization: Bearer TOKEN" \
 -o ca.crl

# Download CRL in DER format
curl http://localhost:8000/api/crl/download/der \
 -H "Authorization: Bearer TOKEN" \
 -o ca.crl

# Get CRL info
curl http://localhost:8000/api/crl/download/info \
 -H "Authorization: Bearer TOKEN"

CRL Info Response:

{
 "status": "available",
 "issuer": "CN=CertMate CA, O=CertMate",
 "last_update": "2024-10-30T18:00:00Z",
 "next_update": "2024-10-31T18:00:00Z",
 "revoked_count": 5,
 "revoked_serials": [12345678,
 87654321
 ]
}

11. Download Domain Certificate Files

Endpoint: GET /api/certificates/<domain>/download

Download certificate files for a specific domain. By default, this endpoint returns a ZIP archive containing all certificate components. A specific file can be requested using the file query parameter. JSON mode is also available for automation that wants all PEMs in one response.

Parameters:

  • domain (Path) - The domain name associated with the certificate.
  • file (Query, Optional) - Specify a single file to download.
    • Supported values: fullchain.pem, privkey.pem, combined.pem
  • format (Query, Optional) - Set to json to return all certificate files in a JSON object.
  • key_format (Query, Optional) - pkcs1 or pkcs8. Certbot writes PKCS#8 (BEGIN PRIVATE KEY); some older stacks require the legacy traditional form. Valid with file=privkey.pem (serves the converted key) or with format=json (adds a converted copy to the response).

Response (200 OK):

  • Default: application/zip (A ZIP file containing all PEM files)
  • With file param: application/x-pem-file (The raw content of the requested file)
  • With format=json: application/json with domain, cert_pem, chain_pem, fullchain_pem, and private_key_pem
  • With format=json&key_format=pkcs1: the above plus private_key_pkcs1_pem

The JSON form is the preferred automation shape for Ansible, Salt, or any other client that wants to write PEM files directly.

key_format=pkcs1 on the JSON form adds private_key_pkcs1_pem and leaves private_key_pem untouched, so an existing consumer is unaffected and a client needing the legacy key no longer has to make a second call and stage it through a file. The field is named for the encoding rather than for RSA: the traditional form of an ECDSA key is SEC1 (BEGIN EC PRIVATE KEY), and CertMate issues ECDSA by default. Key types with no traditional encoding (Ed25519) return 422.

Examples:

# Download all files as a ZIP archive
curl http://localhost:8000/api/certificates/example.com/download \
 -H "Authorization: Bearer TOKEN" \
 -o example_com_bundle.zip

# Download only the fullchain.pem file
curl "http://localhost:8000/api/certificates/example.com/download?file=fullchain.pem" \
 -H "Authorization: Bearer TOKEN" \
 -o fullchain.pem

# Download only the private key
curl "http://localhost:8000/api/certificates/example.com/download?file=privkey.pem" \
 -H "Authorization: Bearer TOKEN" \
 -o privkey.pem

# Download the full certificate bundle as JSON
curl "http://localhost:8000/api/certificates/example.com/download?format=json" \
 -H "Authorization: Bearer TOKEN" \
 -o example_com_bundle.json


12. Reissue Domain Certificate (edit configuration)

Endpoint: POST /api/certificates/<domain>/reissue

Edit a certificate's configuration and reissue it in place — extend or drop SAN entries without delete + recreate. Omitted fields keep the values the certificate was issued with (read from its metadata), so DNS/alias/CA configuration never needs re-entering. The current certificate keeps being served until the reissue succeeds. The key shape is preserved unless explicitly changed (no key flags are sent and certbot keeps the lineage key).

Request Body (all fields optional):

{
  "san_domains": ["www.example.com", "api.example.com"],
  "domain_alias": "",
  "async": true
}
  • san_domains: replacement SAN set — omit to keep, [] to drop every SAN
  • domain_alias: omit to keep, "" to clear
  • dns_provider, account_id, ca_provider, challenge_type: omit to keep
  • key_type/key_size/elliptic_curve: omit to keep the existing key shape
  • async: defer issuance to a background job (202 + job id, poll GET /api/certificates/jobs/<job_id>)

Response (200 OK, or 202 Accepted with async): message, domain, dns_provider, ca_provider, duration.

Errors: 404 when no certificate exists for the domain (use create), 403 scope, 400 validation, 409 operation in progress, 422 certbot failure (the previous certificate is still in place).

Example:

curl -X POST http://localhost:8000/api/certificates/example.com/reissue \
 -H "Authorization: Bearer TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"san_domains": ["www.example.com", "api.example.com"]}'

Log stream (admin, debugging)

GET /api/web/logs/stream

Server-Sent Events tail of the application log file, for watching an issuance or a deployment live from a terminal.

Requires file logging to be on. By default CertMate logs to stdout only — what docker logs and every log shipper expect — so this endpoint reports "Log file not found" until you set CERTMATE_LOG_FILE (e.g. CERTMATE_LOG_FILE=/app/logs/certmate.log). The file is rotated automatically; see the environment table in the README.

curl -N -H "Authorization: Bearer TOKEN" \
 https://certmate.example.com/api/web/logs/stream

Admin-only, because application logs can contain credentials. Only lines written after the connection opens are sent — this is a tail, not a history download. The stream emits a : keepalive comment while idle and closes after 30 idle minutes; an EventSource client reconnects on its own, a curl session has to be restarted.


Error Handling

Error Response Format

Every failure carries a human-readable error and a machine-readable code:

{
 "error": "Certificate not found for domain: example.com",
 "code": "CERTIFICATE_NOT_FOUND"
}

code is always a string. Failures raised by the HTTP layer rather than by the application — an unmatched path, a wrong method, a body over the size limit — carry two more fields, message (the framework's description) and status (the numeric status, which is also the status line):

{
 "error": "Not Found",
 "message": "The requested URL was not found on the server.",
 "code": "NOT_FOUND",
 "status": 404
}

Until the contract version moved to 2.0, code on that second shape was the status integer while every application error used a string, so a client could not branch on the field without checking its type first. It is one type now, and the number a caller may have been reading is in status on those same responses. The version is on every response as X-CertMate-API-Version; it is 2.1 since the async issuance endpoints gained ISSUANCE_QUEUE_FULL.

Codes

Branch on these rather than on the message text, which is written for people and may be reworded.

CodeTypical statusMeans
CERTIFICATE_NOT_FOUND404No certificate for that domain on this instance
CERT_FILE_NOT_FOUND404The certificate exists but the requested file does not
JOB_NOT_FOUND404Unknown async issuance job id
DOMAIN_REQUIRED400The request named no domain
INVALID_REQUEST / INVALID_FORMAT400The body failed validation
INVALID_FILE / INVALID_FILE_TYPE / INVALID_PATH400Bad file argument
INVALID_KEY_FORMAT / KEY_FORMAT_NOT_APPLICABLE / KEY_CONVERSION_FAILED400/422Key export could not be produced in the requested form
AUTO_RENEW_FLAG_REQUIRED400enabled missing from an auto-renew update
INCOMPATIBLE_PARAMETERS400Two request fields contradict each other
AUTH_HEADER_MISSING / INVALID_AUTH_FORMAT / INVALID_AUTH_SCHEME / INVALID_TOKEN / AUTH_ERROR401Authentication failed, and which part
SESSION_REQUIRED401The endpoint needs a browser session, not a bearer token
INSUFFICIENT_ROLE403Authenticated, but the role is too low
DOMAIN_OUT_OF_SCOPE403The API key is scoped to other domains
PRIVKEY_REQUIRES_OPERATOR403Private-key download needs operator or above
CERTIFICATE_ALREADY_EXISTS409A certificate for that domain is already managed
DOMAIN_OPERATION_IN_PROGRESS409Another create/renew holds this domain's lock
METADATA_SCHEMA_DOWNGRADE409metadata.json was written by a newer build; the write was refused
DOMAIN_NOT_IN_SETTINGS409The certificate exists on disk but no settings entry names it
ACME_RATE_LIMITED422The CA refused because a rate limit was reached — waiting is the fix, retrying is the cause
CERTIFICATE_CREATION_FAILED / CERTIFICATE_REISSUE_FAILED / CERTIFICATE_REISSUE_REJECTED422Issuance was attempted and refused
RENEWAL_CONFIG_BROKEN422certbot's renewal config for this lineage no longer resolves; reissue
DNS_ACCOUNT_NOT_CONFIGURED422The DNS account this certificate uses is gone from settings
ISSUANCE_QUEUE_FULL429Too much async issuance is already queued or running; the body carries the depth and the limit
ADOPTION_UNAVAILABLE503Discovery/adoption is not available on this build
ASYNC_ISSUANCE_DISABLED503Async issuance is switched off
CERTIFICATE_CREATION_ERROR / CERTIFICATE_RENEWAL_ERROR / CERTIFICATE_REISSUE_ERROR / CERTIFICATE_DOWNLOAD_ERROR / AUTO_RENEW_UPDATE_FAILED500The operation failed unexpectedly; the server log has the cause
INTERNAL_SERVER_ERROR500An exception escaped a handler
NOT_FOUND, METHOD_NOT_ALLOWED, REQUEST_ENTITY_TOO_LARGE, …4xxRefused by the HTTP layer; the symbol is the status name

Common HTTP Status Codes

CodeMeaningExample
200SuccessCertificate listed
201CreatedCertificate created
400Bad RequestMissing required field
401UnauthorizedInvalid/missing token
403ForbiddenRole or domain scope
404Not FoundCertificate doesn't exist
409ConflictOperation already running
422UnprocessableIssuance refused by the CA
429Too Many RequestsRate limit exceeded
500Server ErrorInternal error
503Service UnavailableOCSP/CRL not available

Example Error

curl http://localhost:8000/api/client-certs/invalid-id \
 -H "Authorization: Bearer TOKEN"

# Response
{
 "error": "Certificate not found: invalid-id",
 "message": "Certificate not found: invalid-id",
 "code": "NOT_FOUND",
 "status": 404
}

Audit Logging

Certificate-lifecycle operations and configuration/access-control changes are recorded to an audit log. This includes the security-relevant lifecycle paths — successful and failed create, renew, reissue, deploy, and auto-renew toggles, plus unattended (scheduler-driven) renewals — each attributed to the actor that performed it and the trigger that caused it.

Log format

The audit log is written to logs/audit/certificate_audit.log. Each line is a standard Python log line whose message is the JSON audit entry:

2026-06-15 18:00:00 - certmate.audit - INFO - {"timestamp": "...", ...}

To recover the JSON, split each line on the literal - INFO - and parse the remainder. Note two time bases: the line prefix timestamp is local server time, while the JSON timestamp field is UTC (ISO-8601). Read it live with:

tail -f logs/audit/certificate_audit.log

Entry shape

{
  "timestamp": "2026-06-15T18:00:00.000000+00:00",
  "operation": "renew",
  "resource_type": "certificate",
  "resource_id": "api.example.com",
  "status": "success",
  "user": "api_key:renew-bot",
  "ip_address": "10.0.0.9",
  "details": {"force": false},
  "error": null,
  "actor": {
    "kind": "agent",
    "id": "9f2c…",
    "label": "api_key:renew-bot",
    "token_prefix": "cm_1a2b",
    "agent_session": "sess-9f2"
  },
  "trigger": {"cause": "agent"}
}
  • actor.kinduser (a human session / OIDC login), api_token (an API key or the legacy global bearer token), agent (an API key explicitly flagged as an AI/MCP agent — see below), scheduler (an unattended renewal job), or system. It is derived only from the authenticated identity.
  • actor.id / token_prefix — the stable API key id and token prefix behind the action (absent for the legacy global bearer token, which cannot be told apart per-caller — prefer scoped keys).
  • actor.agent_session / agent_id — the values of the client-supplied X-CertMate-Agent-Session / X-CertMate-Agent-Id headers (the MCP server sends them). These are an informational claim only: they are recorded for correlation but never change actor.kind, so a non-agent caller cannot forge an agent attribution.
  • trigger.causemanual, api, agent, scheduled_renewal, or event; for scheduled renewals trigger.job_id names the job.

To have an agent's actions recorded as actor.kind="agent", create a scoped API key with is_agent: true (a checkbox on Settings → API Keys, or is_agent in POST /api/keys) and point the MCP server at it. See the MCP guide.

Reading the audit log over the API

GET /api/activity?limit=N returns the most recent entries (admin/viewer, bounded to 500).

Tamper-evidence (hash chain)

Alongside the human-readable log, every entry is appended to a tamper-evident SHA-256 hash chain at data/audit/certificate_audit.chain.jsonl. Each record is {seq, entry, prev_hash, hash} where hash commits to the entry and the previous record's hash, and seq is a gap-free counter — so any modification, deletion, or reorder by anyone who cannot recompute the whole chain is detectable and localizable. It is on by default; disable with CERTMATE_AUDIT_CHAIN=0.

Verify from the API: GET /api/audit/verify (admin) returns the verifier result and HTTP 200 when intact or 409 when broken:

{"ok": true, "count": 128, "first_seq": 0, "last_seq": 127, "head_hash": "5ee1…", "reason": "intact"}

Verify off-box: the standalone verifier depends only on the Python standard library, so an auditor can run it without installing or trusting CertMate:

python -m modules.core.audit_verify data/audit/certificate_audit.chain.jsonl
# OK: audit chain intact (128 entries, seq 0..127)
# or: FAIL: audit chain broken at seq 42: hash mismatch at seq 42: entry was modified

Exit code 0 intact, 1 broken (with the offending seq and reason), 2 missing/unreadable.

Signed export bundle (third-party verifiable)

The instance holds an Ed25519 signing key, persisted at data/.audit_signing_key (generated on first run, 0600; override with AUDIT_SIGNING_KEY_FILE to hold it off-box). Its public identity is exposed at GET /api/audit/public-key (admin): {algorithm, public_key_pem, fingerprint}. The chain head is signed into periodic checkpoints (certificate_audit.checkpoints.jsonl).

GET /api/audit/export (admin, optional ?from_seq/?to_seq) returns a signed, self-verifying bundle — {manifest, entries, bundle_signature}. The manifest pins the instance fingerprint, public key, seq range and head_hash; the signature is over the canonical manifest, which (via head_hash) transitively commits to every entry. An auditor verifies it off the box without running or trusting CertMate, optionally pinning the key out of band:

python -m modules.core.audit_verify --bundle bundle.json --pubkey instance.pem
# OK: audit bundle intact and signed (128 entries, seq 0..127; signed by 0m2V5lDmnkPWOUHX)

The verifier checks the chain structure, that the manifest matches the entries, the Ed25519 signature, and that the fingerprint matches the (optionally pinned) public key.

Partial slices. A full export starts at the genesis and is format_version: 1. A slice that starts mid-chain (?from_seq=N past the first entry) is format_version: 2 and additionally carries anchor_prev_hash / anchor_seq in the manifest — the predecessor hash its first entry continues from — so the fragment can be verified even though it has no genesis. The anchor is inside the signed manifest, so the signature attests it. The verifier reports such a bundle as a partial slice and names the anchor seq: it proves the entries from the anchor forward are authentic and ordered, and proves nothing about what came before. Verifiers older than v2.23.0 report unsupported bundle format_version 2 for an anchored slice; full exports remain byte-compatible with them.

Threat-model honesty. The chain + signature detect any interior modification, deletion, or reorder, and tie an export to this instance's public key — for anyone who does not hold the signing key. They do not bind the operator, who holds the key and could re-sign a rewritten chain, and tail truncation is only caught by comparing exports over time (a later export with fewer entries) or against an externally held checkpoint. Fully constraining the operator requires shipping the signed checkpoints to an external append-only sink — opt-in external anchoring, a planned follow-up not yet shipped. See compliance.md.


Certificate Types

API mTLS

For API client authentication via mutual TLS.

cert_usage: "api-mtls"

VPN

For VPN client authentication.

cert_usage: "vpn"

Custom Usage Types

You can use any custom usage type string:

cert_usage: "custom-application"

Best Practices

Security

  1. Protect Your Token
  • Keep tokens secret
  • Rotate tokens regularly
  • Use HTTPS in production
  1. Certificate Management
  • Enable auto-renewal
  • Monitor expiration dates
  • Review audit logs regularly
  • Revoke compromised certs immediately
  1. Rate Limiting
  • Respect rate limits
  • Implement exponential backoff
  • Batch operations when possible

Performance

  1. Use Batch Operations
  • Import multiple certs at once
  • Reduces API calls
  • Better error reporting
  1. Filter Results
  • Use query parameters
  • Filter by usage or status
  • Reduces data transfer
  1. Cache When Appropriate
  • Cache certificate metadata
  • Refresh periodically
  • Check expiration locally


← Back to DocumentationQuick Start →Architecture →