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:
-
userresolves the API token to a human identity. The token'screated_byfield 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_addressis 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". -
detailsis 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. -
erroris 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:
| Event | When it fires |
|---|---|
certificate_created | First issuance for a domain. |
certificate_renewed | Successful renewal (new serial number). |
certificate_revoked | Revocation against the CA's OCSP endpoint. |
certificate_downloaded | Anyone fetched the cert / key over the API. |
batch_operation | Bulk issue/renew/revoke kicked off. |
api_request | Non-state-changing API calls (optional, configurable). |
deploy_hook_fired | A hook was invoked + exit code + stdout/stderr (truncated). |
config_change | Settings, DNS accounts, deploy hook configs. |
auth_failure | Invalid 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_changerow with the field that changed and the new value. The previous value is in the latestbackup_*.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.pemwith 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:
-
Set
AUDIT_RETENTION_DAYSto your compliance requirement, setAPI_BEARER_TOKENto 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). -
Ship the audit log to your SIEM with a real-time tail (Vector,
Filebeat, Promtail) — file path
/app/logs/audit/certificate_audit.log. -
Wire alerts on
auth_failurebursts (signal of credential probing) andrenew/errorrepeats (signal of a broken DNS path before the cert expires). - 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.