Skip to content

Latest commit

 

History

History
891 lines (692 loc) · 25.5 KB

File metadata and controls

891 lines (692 loc) · 25.5 KB

CertMate Client Certificates - API Reference

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:

Endpoint Limit Per
General 100 minute
Create Certificate 30 minute
Batch Operations 10 minute
OCSP Status 200 minute
CRL Download 60 minute
Per-IP ceiling 600 minute

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

{
 "error": "Error message",
 "code": "ERROR_CODE",
 "status": 400
}

Common HTTP Status Codes

Code Meaning Example
200 Success Certificate listed
201 Created Certificate created
400 Bad Request Missing required field
401 Unauthorized Invalid/missing token
404 Not Found Certificate doesn't exist
429 Too Many Requests Rate limit exceeded
500 Server Error Internal error
503 Service Unavailable OCSP/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",
 "code": 404,
 "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