Aller au contenu principal

Audit Log

Certeasy writes a tamper-evident audit log of business decisions and security-relevant events: account lifecycle, order creation, authorization outcomes, certificate issuance and revocation, rate-limit denials, and license transitions. Each line is HMAC-chained to the previous one so any insertion, deletion, or modification is detectable after the fact with the bundled certeasy audit verify command.

The audit log is enabled by default: a silent opt-out would be a compliance hole. Disable it explicitly with enabled: false if you have a specific reason.

Configuration

audit:
enabled: true
path: ""
rotate:
max-size-mb: 0
max-backups: 0

Omitting the audit block applies the defaults shown above.

FieldDefaultMeaning
enabledtrueSet to false to disable the audit writer entirely. No file is created and Audit() calls are dropped silently.
path""Absolute path to the audit log file. Empty resolves to <workdir>/audit.log.
rotate.max-size-mb0When greater than zero, Certeasy rotates the file in-process once it exceeds this size (using the same writer as logs.rotate). 0 means "no in-process rotation" — let the OS handle it (logrotate on Linux, Task Scheduler on Windows).
rotate.max-backups0Number of rotated backups to keep when in-process rotation is active. 0 deletes the previous file on rotation. Ignored when max-size-mb is 0.

Choosing in-process vs OS-managed rotation

In-process rotation is convenient on Windows where logrotate is not available out of the box. It uses the same logx.RotatingFileWriter as the application log: rotated files are renamed <path>.1, <path>.2, …, with .1 being the most recent backup.

OS-managed rotation is the recommended setup on Linux: drop a logrotate snippet that uses copytruncate so Certeasy's append-mode handle is not invalidated mid-rotation. Either way, certeasy audit verify walks rotated files automatically — the chain is preserved across rotations.

What Is Logged

The audit log captures decisions and security events. The list is intentionally narrow: nonce churn, individual JWS verifications, GET requests on the directory, and /metrics scrapes are explicitly excluded — they would drown the signal in noise without forensic value.

The tables below list every recorded event, what triggers it, and what extra context is attached in the details field. Common fields (ts, account_id, source_ip, user_agent) are described in Line Format and are not repeated here.

Account events

EventWhenDetails
account.createA new ACME account is created. The dedupe path (existing key reused) is silent.contact: list of contact URIs declared by the client
account.keychangeAn account's JWK is rotated successfully via keyChange.old_thumbprint, new_thumbprint
account.deactivateAn account is set to deactivated by its owner.(none)

Order and authorization events

EventWhenDetails
order.createA new order is accepted and persisted.order_id, dns_names (canonical: lowercased, sorted, deduped, wildcards preserved)
order.finalizeA finalize request has been accepted and the issuance job is enqueued. Represents the client's accepted finalize, not the actual cert issuance.order_id, policy_id, dns_names
order.invalidAn order transitions to invalid. Emitted at most once per order — retries do not duplicate the event. The free-form reason field on the line carries the failure message.order_id, source: "challenge" or "pki"
authorization.validateAn authorization transitions to valid (decision: allow) or invalid (decision: deny). Deny is emitted only when the authorization actually flips to invalid (i.e. all challenges of that authz failed).authz_id, identifier, chall_type
challenge.validateA challenge succeeds. Failures are not recorded per challenge — they would be too noisy. The chain "these challenges passed → this cert was issued" remains reconstructible from the order, authorization, and certificate events.chall_id, chall_type, identifier, authz_id

Certificate events

EventWhenDetails
certificate.issueA certificate has been issued and persisted.cert_id, order_id, pki_request_id, serial, dns_names
certificate.revokeA certificate is revoked via the revoke-cert endpoint.cert_id, fingerprint, reason (RFC 5280 reason code; JSON null when the client did not provide one)

Rate limiting

EventWhenDetails
ratelimit.denyA request is refused with HTTP 429. Emitted once per refusal.type: one of global, account-creation, order-creation, duplicate-certificate, failed-validation, pending-authorizations; retry_after (seconds)

License

EventWhenDetails
license.changeThe license state transitions between valid, grace, expired, revoked, or no_license. The boot baseline is not emitted — only subsequent transitions are.prev, next

Line Format

The log is JSONL — one self-contained JSON object per line. Field order is fixed by the schema version (currently "1").

{"ts":"2026-05-08T14:32:11.123Z","schema":"1","seq":12345,"prev_mac":"hmac-sha256:abcd...","event":"order.create","account_id":"acct_abc","source_ip":"10.0.0.5","user_agent":"certbot/2.9","decision":"allow","details":{"order_id":"ord_xyz"},"mac":"hmac-sha256:1234..."}
FieldAlways presentDescription
tsyesRFC 3339 timestamp with millisecond precision, in UTC
schemayesSchema version string. Currently "1". Bumped only on incompatible layout changes.
seqyesStrictly increasing per installation. Resumes across restarts and rotations.
prev_macyesThe mac of the previous line, prefixed with hmac-sha256:. The first line uses the genesis MAC anchored on the installation ID.
eventyesDot-namespaced event name (order.create, certificate.revoke, …)
account_idoptionalACME account identifier when the event is account-scoped
source_ipoptionalClient IP (see RGPD note below)
user_agentoptionalClient User-Agent string
decisionoptionalallow or deny for events that involve a policy check
reasonoptionalHuman-readable reason — empty when not applicable
detailsoptionalEvent-specific payload, encoded as a JSON object
macyeshmac-sha256:<hex> of all preceding fields. Always the last field on the line.

Optional fields are omitted from the line when empty (omitempty).

How the Chain Works

On the very first install, Certeasy generates a 32-byte random secret and stores it in the database (audit_state table). The secret is never logged, never exposed via any API, and never rotated.

The first line of every installation is anchored to a genesis MAC computed from the secret and the stable installation identifier:

genesis_mac = HMAC-SHA-256(secret, "certeasy-audit-v1|" + installation_id)

Each subsequent line carries the mac of the previous line in its prev_mac field, and computes its own mac as HMAC-SHA-256(secret, line_bytes_without_mac). Tampering with any line invalidates that line's MAC; tampering with the chain (insertion, deletion, reordering) invalidates the next prev_mac.

The genesis anchor matters: an audit file restored on a different installation (different installation_id) will not validate, even if the secret matches. This is intentional — it prevents a stolen audit file from being passed off as evidence on another system.

Why HMAC and not plain SHA-256

A plain hash chain seeded from a publicly known value (the installation ID is visible in logs and the license portal) would let anyone with write access to the file rebuild the chain after modifying a line. HMAC requires the secret stored in the database — without DB access, the chain cannot be reforged.

The threat model: filesystem compromise alone does not allow forgery. DB compromise is a higher bar; if an attacker has read/write access to the database, the audit log is no longer the weakest link.

Verifying the Chain

The chain is only useful if you actually verify it. Run:

certeasy audit verify -f /etc/certeasy/config.yml

The command walks rotated predecessors (audit.log.1, .2, …) chronologically, then the active file. It validates:

  1. Each line's mac against HMAC(secret, line_bytes_without_mac).
  2. Each prev_mac against the previous line's mac (or the genesis MAC for line 1).
  3. That seq is strictly increasing.

Exit codes:

CodeMeaning
0Chain valid (or no audit lines exist yet — fresh install)
1Chain broken. The first break is reported on stderr with file, line number, and reason.

You can override the file path:

certeasy audit verify -f /etc/certeasy/config.yml --path /backups/2026-05/audit.log

This still requires the database (the secret and the installation ID live there), so the override is for verifying a copy of the file alongside the live DB — not for verifying a backup on a different machine. To verify a snapshot offline, restore the DB backup alongside the audit file first.

When to verify

  • Periodically (e.g. nightly via cron / Task Scheduler) — catch silent corruption early.
  • After every restore — confirm the audit file and the database secret are consistent.
  • When investigating an incident — confirm the timeline you are reading was not modified after the fact.

Listing nodes that wrote to the chain

Each audit line carries the server_id of the node that wrote it (see Node identity). To list every node that has ever booted against this database — useful when investigating who wrote which lines, or before decommissioning a host:

certeasy audit list-servers -f /etc/certeasy/config.yml

Output columns: server_id, hostname, first_seen, last_seen (UTC, RFC 3339). Sorted by last_seen descending so the most recently active node appears first.

Storage and Backups

The HMAC secret lives in the database. Backing up the database is required for the audit log to remain verifiable: the audit file alone is useless without the secret. Cover both in the same backup procedure — see the Backup page.

Loss of the secret means loss of verifiability for all earlier lines (the file is still readable, just no longer cryptographically anchored). New writes cannot resume the old chain; on a fresh install, the writer starts a new chain from a new genesis.

Personal Data and RGPD

source_ip and user_agent are potential personal data. Retention and the user-facing notice are the responsibility of the operator who installs and runs Certeasy:

  • Retention — Certeasy does not delete audit lines on its own. The retention policy is whatever your OS rotation rule keeps.
  • Notice — Mention the audit log in your service's privacy notice.
  • Access controls — The audit file is created with 0644 permissions by default. Restrict the directory if your hosting model requires it.

There is no PII redaction option in v1: the goal of the audit log is forensic, and redacted entries would defeat that goal. Operators who cannot retain IPs should disable the audit log entirely (enabled: false) and accept the loss of forensic capability.

Operational Notes

  • Failures do not block business flow. If a write to the audit file fails (full disk, permission error), the failure is logged via the audit log service and the operation continues. Audit gaps are detected by audit verify, not by ACME clients.
  • Line size cap. Lines are capped at 1 MiB. Events that would produce a larger line are dropped with a log entry. The cap is a defence against a misbehaving event source — legitimate events are well below 1 KiB.
  • No auto-purge. Even when in-process rotation is enabled, Certeasy never deletes lines based on age. Retention is purely a function of how many rotated backups you keep (max-backups or your OS rotator).