Operations
Renewal
Section titled “Renewal”CertVault checks at startup and every six hours for certificates due for renewal or whose issued SAN set or key type differs from configuration. SAN additions and removals trigger issuance; domain order and DNS letter case do not. Desired configuration does not change the current downloadable version: the previous version remains available until replacement issuance succeeds. The console distinguishes issued properties from configured properties. Older versions without key-type metadata are inspected from their stored public certificate. acme.automatic_issuance controls automatic issuance globally, and each certificate may override it with automatic_issuance.
Only one issuance runs at a time because DNS-provider construction consumes process environment. A failed renewal leaves the previous certificate version untouched. New issuance normalizes ACME responses into a leaf-only certificate.crt, a deduplicated chain.crt, and fullchain.crt containing the leaf followed by that chain. Renew existing certificates to generate normalized artifacts.
Audit persistence
Section titled “Audit persistence”API-key creation, revocation, and deletion through the console or CLI require an audit entry in the same SQLite transaction. If auditing fails, the mutation rolls back and the request or command fails; a newly created token is not returned. Console actions record the authenticated administrator’s identity (OIDC email, or subject when email is absent); CLI actions use local-cli.
Authentication, downloads, renewal admission, issuance completion, and ACME-account file deletion use best-effort auditing. An audit write failure does not undo these operations. Every failed audit write emits an error log with actor, action, resource, and database error. ACME-account deletion spans the filesystem and database and is not atomic with its audit record; the deletion can succeed even when audit persistence fails. Audit records and these logs do not contain API-key tokens or private keys.
Audit retention
Section titled “Audit retention”Audit events are retained indefinitely by default. Enable automatic cleanup with:
audit: retention: 90dExpired events are removed once at startup and every 24 hours afterward.
Choose retention based on your audit history requirements and daily event volume.
Each artifact download returning HTTP 200 adds an event; unchanged downloads
returning 304 do not. Authentication, issuance, and administrative actions add
events too. For example, 10,000 events per day with 90d retention means roughly
900,000 retained rows, plus events awaiting the next daily cleanup. This is a
capacity-planning example, not a tested capacity limit. Omitting retention allows
the table to grow without a bound.
SQLite indexes audit timestamps for retention and actors, actions, and resources for exact filters. Existing databases receive these indexes during startup migration; building them takes longer with larger audit histories. Filter choices use covering indexes, which still scan index entries as history grows. Free-text search matches substrings across actors, actions, resources, details, and IP addresses; ordinary indexes cannot accelerate the leading-wildcard match. Apply exact filters alongside text search to narrow the rows examined. Pagination bounds the returned rows, but matching totals and deep page offsets can still require substantial work.
Monitor audit row counts, database size, and query latency at your expected volume. Database operations share one connection, so long searches and large retention deletions can delay other requests. Deleted SQLite space can be reused; retention does not automatically shrink the database file.
To compare indexed and unindexed queries against a synthetic 100,000-event
history, run from backend/:
go test ./database/repository -run '^$' -bench BenchmarkAuditQueries100K -benchmemThe benchmark covers exact filters, filter choices, and substring searches with and without an exact filter. Its timing depends on the machine and event distribution; repeat it with representative data before choosing a retention window for a busy deployment.
Supported events are:
certificate.issuedcertificate.renewedcertificate.failed
Hook delivery runs asynchronously and does not block certificate issuance. The default timeout is 15 seconds.
The optional certificates list limits a hook to named certificates. Omit it or leave it empty to receive matching events for every configured certificate. Unknown names are rejected during configuration validation.
Webhook example
Section titled “Webhook example”hooks: - name: certificate-automation type: webhook events: - certificate.issued - certificate.renewed - certificate.failed certificates: - homelab-wildcard url: https://automation.example.com/hooks/certvault secret_file: /run/secrets/certvault_webhook_secret timeout: 15sCertVault sends an HTTP POST with Content-Type: application/json. A successful receiver must return a 2xx response. An issued-certificate payload resembles:
{ "id": "evt_1770000000000000000", "event": "certificate.issued", "timestamp": "2026-02-02T02:40:00Z", "certificate": "homelab-wildcard", "version": { "id": 42, "certificate_name": "homelab-wildcard", "serial": "01A2B3C4", "not_before": "2026-02-02T02:39:00Z", "not_after": "2026-05-03T02:39:00Z" }}Failed events include an error field and may have a null version.
When secret_file is configured, the request includes:
X-CertVault-Signature-256: sha256=<hex HMAC-SHA256 of body>The receiver should calculate HMAC-SHA256 over the raw request body with the shared secret and compare signatures using a constant-time operation.
Executable hook example
Section titled “Executable hook example”hooks: - name: local-event-log type: exec events: - certificate.issued - certificate.renewed - certificate.failed certificates: - homelab-wildcard command: /hooks/record-event args: - /data/hook-events.jsonl timeout: 10sMount the executable read-only into the container:
services: certvault: volumes: - ./hooks/record-event:/hooks/record-event:roAn example record-event script is:
#!/bin/shset -eu
destination=${1:?destination is required}printf '%s\n' "$CERTVAULT_EVENT_JSON" >> "$destination"Make it executable before starting the container:
chmod 0755 hooks/record-eventExecutable hooks run directly without an implicit shell. They receive only PATH=/usr/local/bin:/usr/bin:/bin and CERTVAULT_EVENT_JSON, and are terminated at the configured timeout. The standard image is intentionally minimal, so mount a self-contained executable or script and any additional tooling it requires.
Health endpoints
Section titled “Health endpoints”/api/v1/healthreports process health and the running application version./api/v1/readyverifies that required infrastructure is available.
Console refresh and database load
Section titled “Console refresh and database load”The console refreshes visible resources every 30 seconds, with certificate refreshes every two seconds while a known issuance job is queued or running. Certificate data is also loaded for the API-key certificate selector. Inactive pages do not continually refresh their resource lists. A lightweight session check accompanies the infrastructure refresh so authentication expiry is still detected on pages without certificate polling. Hidden tabs pause automatic refresh and refresh immediately when visible again. Requests time out after 15 seconds, polling waits for completion, and a mutation refresh cancels an older request for the same resource. Changes initiated elsewhere can take up to the idle refresh interval to appear on a visible page.
Certificate listing uses three database queries: enabled certificates, latest jobs, and current versions. Version selection uses the existing certificate/version index; equal creation timestamps are resolved by descending version ID consistently with downloads. Already-loaded certificate rows supply association names. API-key authorization reads current permissions, expiry, and revocation on every request, but usage metadata is sampled at most once per minute per key. The displayed last-used time and IP describe the most recent sample, rather than every request.
The SQLite connection pool remains at one connection. Reproduce the list benchmark from backend with:
go test ./database/repository -run '^$' -bench '^BenchmarkCertificateList$' -benchtime=100xA local run on an AMD Ryzen 7 9700X with three versions and jobs per certificate measured approximately 0.27, 1.19, and 11.36 milliseconds per list operation for 10, 100, and 1,000 certificates with one client. Eight concurrent clients achieved approximately 104 list operations per second at 1,000 certificates (9.64 milliseconds per operation as aggregate throughput, not individual request latency). These measurements cover repository work with synthetic data, excluding HTTP, authentication, serialization, and browser rendering; use representative production histories and workloads before changing the connection strategy.