6 min read Grounded in docs/architecture.md Audit Logging

Audit log and compliance

If a compliance auditor asks 'who renewed cert X on date Y and from what IP', CertMate answers in one query. Here's the schema, what's captured, what isn't, and how to wire the audit log into the rest of your SIEM.

Certificate management touches the operationally sensitive parts of a business: production hostnames, deployment automation, the keys that prove your identity on the public internet. In any environment with a compliance regime — SOC 2, ISO 27001, PCI, internal change- management — the question "who changed what, when" is going to be asked.

CertMate writes one structured audit row per state-changing operation. The schema is stable, the queries are cheap, and the data is enough to answer the questions an auditor actually asks.

The schema

Every audit entry is a JSON document with a fixed shape:

{
  "timestamp": "2026-05-17T18:00:00Z",
  "operation": "create" | "renew" | "revoke" | "delete" |
               "download" | "deploy" | "config_change" | ...,
  "resource_type": "certificate" | "client_cert" | "backup" |
                   "dns_account" | "setting" | "deploy_hook",
  "resource_id": "example.com",
  "status": "success" | "error",
  "user": "alice@example.com",
  "ip_address": "10.0.0.42",
  "details": {
    "common_name": "example.com",
    "san_domains": ["*.example.com"],
    "dns_provider": "cloudflare",
    "dns_account_id": "production",
    "serial_number": "04:5e:a1:..."
  },
  "error": null
}

The fields worth specifically calling out:

  • user resolves the API token to a human identity. The token's created_by field carries the email of the operator who minted it; CertMate joins that in at log-write time so the audit row is human-readable without a lookup.
  • ip_address is the source IP of the HTTP request — or the loopback when CertMate's scheduler is acting on its own (renewals). Useful to distinguish "human action" from "scheduled job".
  • details is a free-form object whose shape depends on the operation. Renewals carry the new serial; deploys carry the hook id; config changes carry a diff of the field that changed.
  • error is null on success, populated on failure with the upstream error verbatim. This is the field your audit query grep against when investigating an incident.

What gets written

CertMate's auditor module exposes specific log methods for the events that matter:

EventWhen it fires
certificate_createdFirst issuance for a domain.
certificate_renewedSuccessful renewal (new serial number).
certificate_revokedRevocation against the CA's OCSP endpoint.
certificate_downloadedAnyone fetched the cert / key over the API.
batch_operationBulk issue/renew/revoke kicked off.
api_requestNon-state-changing API calls (optional, configurable).
deploy_hook_firedA hook was invoked + exit code + stdout/stderr (truncated).
config_changeSettings, DNS accounts, deploy hook configs.
auth_failureInvalid token, scope rejection, rate limit hit.

The full method list lives in modules/core/audit_logger.py. The non-obvious one is auth_failure: every rejected request (bad bearer token, out-of-scope domain on a scoped key) writes an audit row with the IP and the attempted resource. This is what you grep when an auditor asks "show me the unauthorized access attempts."

Queryable patterns

CertMate exposes the audit log via REST (GET /api/audit with filters on operation, resource, user, status, time range) plus a UI timeline view per certificate. The patterns auditors actually ask for:

"Who renewed cert X in the last 90 days?"

curl -G http://localhost:8000/api/audit \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "operation=renew" \
  --data-urlencode "resource_id=example.com" \
  --data-urlencode "since=$(date -u -v-90d +%Y-%m-%d)"

"Show me every failed renewal in the last week"

curl -G http://localhost:8000/api/audit \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "operation=renew" \
  --data-urlencode "status=error" \
  --data-urlencode "since=$(date -u -v-7d +%Y-%m-%d)"

"Who has access to issue against *.example.com?"

This isn't really an audit-log question — it's an authorization question. The answer comes from the API key registry:

curl http://localhost:8000/api/auth/keys \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.[] | select(
        .role == "operator" and
        (.allowed_domains // []) | any(. as $d | "example.com" | inside($d))
      )'

Retention and export

CertMate keeps audit entries in the local sqlite store with a configurable retention window. The default is "forever" — auditors appreciate this, your disk doesn't, so set AUDIT_RETENTION_DAYS based on your compliance requirement (90 days for general operations, 365+ for regulated workloads).

Export the log periodically to your SIEM / log aggregator so the audit trail survives a CertMate disaster:

# Tail the audit log file directly (JSONL format)
tail -f /app/logs/audit/certificate_audit.log | \
  vector --config /etc/vector/certmate.toml

# Or pull via API, paginated
curl -G http://localhost:8000/api/audit \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "since=$LAST_RUN" \
  --data-urlencode "limit=1000"

The on-disk format is line-delimited JSON — every line is one entry — which lines up with how virtually every log shipper expects to ingest data. jq, Splunk, Elastic, Datadog, Loki, Vector: they all read this without configuration.

What's not in the audit log (and where to look)

  • Configuration before the change. CertMate writes a config_change row with the field that changed and the new value. The previous value is in the latest backup_*.zip — CertMate auto-creates a backup immediately before every config change, exactly so you can answer "what did it look like before?"
  • The bytes of the private key. The audit log records that a key was generated or rotated and what its fingerprint is. The actual bytes live in /app/certificates/<domain>/privkey.pem with mode 600 and never enter the audit stream.
  • HTTP request bodies. CertMate logs that a request happened, what user, against what resource, with what outcome. It doesn't log the full request body — that would be a PII-exfiltration risk in a multi-tenant deployment.

Wiring this into a compliance program

The basics, in order of effort:

  1. Set AUDIT_RETENTION_DAYS to your compliance requirement, set API_BEARER_TOKEN to a long random value, set up at least one operator-role API key per human who needs CertMate access (don't share the admin token).
  2. Ship the audit log to your SIEM with a real-time tail (Vector, Filebeat, Promtail) — file path /app/logs/audit/certificate_audit.log.
  3. Wire alerts on auth_failure bursts (signal of credential probing) and renew/error repeats (signal of a broken DNS path before the cert expires).
  4. Run CertMate's signed container image (every release ships with cosign signature + SLSA L3 provenance — see the release page) so the binary you're auditing is the binary that left CI.

None of this is novel. It's the same audit posture you'd expect from any well-run operational tool. The point is that CertMate ships it as a contract, not as something you bolt on.