The openvox-ca server ships with an optional Prometheus exporter. When enabled it serves the standard Go runtime/process metrics and HTTP request metrics expected of a Go web service, plus CA-specific series describing the CA certificate, its CRL, and every known (non-deleted) leaf certificate — including issue/expiry timestamps and issuance status.
A ready-to-import Jsonnet alerting mixin is included; see Alerting for what it covers.
The exporter is disabled by default. Enable it by setting a listen address:
| Flag | Env | Config (YAML) |
|---|---|---|
--metrics-listen 127.0.0.1:9140 |
PUPPET_CA_METRICS_LISTEN=127.0.0.1:9140 |
metrics_listen: 127.0.0.1:9140 |
openvox-ca --cadir /var/lib/puppet-ca --metrics-listen 127.0.0.1:9140The exporter runs on a separate listener from the Puppet API and always
serves plain HTTP at /metrics, regardless of the API's TLS configuration. In
the default isolated-process mode it runs inside the frontend process (the
signer process has no network exposure).
Security: the leaf-certificate metrics expose node hostnames (certificate subjects) as label values. Bind the exporter to loopback or a trusted management network — e.g.
127.0.0.1:9140scraped via a node exporter sidecar, or a dedicated interface protected by a network policy — rather than a public address.
scrape_configs:
- job_name: openvox-ca
static_configs:
- targets: ['openvox-ca.internal:9140']The job_name is referenced by the alerting mixin's selector (default
job="openvox-ca").
All CA-specific metrics use the puppetca_ prefix. Timestamps are seconds since
the Unix epoch, the Prometheus convention for *_timestamp_seconds gauges.
| Metric | Description |
|---|---|
go_* |
Go runtime metrics (goroutines, GC, memory) from the standard Go collector. |
process_* |
Process metrics (CPU, resident memory, open FDs) where supported by the platform. |
puppetca_http_requests_total{method,code} |
Total CA API requests, by HTTP method and response code. |
puppetca_http_request_duration_seconds{method,code} |
CA API request latency histogram. |
puppetca_http_requests_in_flight |
CA API requests currently being served. |
HTTP request metrics are intentionally not labelled by URL path: the CA's API paths embed per-node subjects (e.g.
/certificate_status/<hostname>), which would otherwise explode metric cardinality.
| Metric | Description |
|---|---|
puppetca_ca_ready |
1 when the CA has finished initialising, else 0. |
puppetca_collector_scrape_success |
1 if the last CA-state gather succeeded, else 0 (e.g. storage unavailable). |
puppetca_collector_scrape_duration_seconds |
Time taken to gather the CA, CRL and leaf metrics. |
| Metric | Labels | Description |
|---|---|---|
puppetca_ca_certificate_info |
common_name, serial, issuer |
Constant 1; carries CA identity in labels. |
puppetca_ca_certificate_not_before_timestamp_seconds |
— | CA certificate issue time. |
puppetca_ca_certificate_not_after_timestamp_seconds |
— | CA certificate expiry time. |
| Metric | Description |
|---|---|
puppetca_crl_number |
Monotonic CRL sequence number (cRLNumber). |
puppetca_crl_this_update_timestamp_seconds |
CRL ThisUpdate time. |
puppetca_crl_next_update_timestamp_seconds |
CRL NextUpdate (expiry) time. |
puppetca_crl_revoked_certificates |
Number of certificates currently listed in the CRL. |
puppetca_crl_update_failures_total |
Counter of failures to amend the CRL. A lower bound — see the note below. Resets to 0 on process restart. |
puppetca_crl_cached_number |
CRL number of the copy this replica is answering revocation checks from. number, this_update, next_update and revoked_certificates above are read from storage and so are identical on every replica; this one and both *_failures_total counters are per-process and have to be checked on each. |
puppetca_crl_sync_failures_total |
Counter of failures to reload the stored CRL into that in-memory copy — an unreadable or unparseable CRL, or one this CA did not sign. While it rises the replica keeps enforcing whichever CRL it already held. Resets to 0 on process restart. |
Each replica decides revocation from puppetca_crl_cached_number and reloads it
from storage every crl_sync_interval_sec (60s by default), so after a
revocation it briefly trails puppetca_crl_number. A gap that persists is a
replica still admitting certificates the rest of the fleet has revoked — see
watching revocation propagate below for the
query, and puppetca_crl_sync_failures_total for why it is stuck.
What the failure counter does and does not see. It counts a CRL that could not be re-signed or written, or read, on any of the four paths that write one (revoke, CRL reissue, background refresh, expired-cert cleanup). The read half is
readStoredCRL's doing: it increments before returning, on every path that calls it, which is what makes this counter cover all four rather than only the one that noticed. On the revoke path alone it also counts a malformed serial, or an inventory read that failed while resolving the subject's serial. That last one includes a revocation whose wait for the per-subject lock spent the 60-second budget behind another goroutine in this process on the single-node backends, which reach the read with the deadline already gone; see revocation cost. So a queued revocation is a benign cause of this alert on filesystem and SQLite. A wait for another process on the host is refused at acquisition instead and never reaches the read, so it does not move this counter — see the uncounted list below. The revoke path is the shared revoke-by-serial code, so it also coversDELETE /certificate_status(puppet cert clean) and the best-effort revocation of a superseded certificate on renewal, which on a busy fleet is the likeliest source of a rising count: grep forRenew:/AutoRenew: failed to revoke replaced certificatealongside theClean:warnings. Both renewal warnings name the serial, and the certificate they left valid is the one case revoking by subject cannot reach — the replacement is what makes it a renewal, sorevoke --certnamewould retire that instead. Retire it withopenvox-ca-ctl revoke --serial <hex>; see revocation by serial. TheClean:warnings are qualified differently: they name the serial only once the revocation reached the CRL, and the certificate they leave behind is still reachable by subject until a replacement is issued.Uncounted, and logged only: a revocation refused at a lock acquisition, which fails ahead of any CRL work (this is the
409a spent budget produces on PostgreSQL, MySQL, etcd and Redis — the single-node backends take the lock and fail later, which is counted, as above, unless another process on the same host was holding it, the one case where they too refuse at acquisition), a subject that was simply never issued — thoughPUT /certificate_statusalso answers its caller409in both cases — and a malformed serial met by the cleanup job.A background refresh that cannot read the CRL does move this counter, by the rule above. What it stays invisible to is
puppetca_collector_scrape_success: the exporter drops the CRL gauges and still reports a successful scrape. It does not show up as an expiring CRL either, because the gauge is absent rather than stale — the shippedPuppetCACRLExpiringSoonandPuppetCACRLExpiredrules comparepuppetca_crl_next_update_timestamp_secondsagainsttime()and match nothing when the series is missing. Pair them with a per-instance presence check, e.g.puppetca_ca_ready == 1 unless on(instance) puppetca_crl_next_update_timestamp_seconds; bareabsent()will not fire while any other replica still reports the series.
The four CRL gauges above describe the first block of the stored blob, which is normally this CA's own CRL and not always: a hand-assembled blob can lead with an ancestor's, and the CA then answers revocation from its own block found later in the chain (see storage internals) while these series still describe block 0. Startup warns loudly in that state, and every write path refuses it, so it is a condition to fix rather than to monitor. When a CRL chain has been imported, the ancestor CRLs that follow it are not covered: this CA cannot re-sign them, so their expiry is not something a refresh can fix and not something these series track.
The shipped CRL expiry alerts do not cover ancestor CRLs.
PuppetCACRLExpiringSoonandPuppetCACRLExpiredreadpuppetca_crl_next_update_timestamp_seconds, which is block 0 — and the background refresher keeps block 0 perpetually fresh. So an ancestor CRL can lapse, breaking full-chain revocation checking for every agent running Puppet's defaultcertificate_revocation = chain, while both alerts stay green. Until a chain-aware series exists, track ancestornextUpdatedeadlines out of band and re-import before they lapse.No series tracks the chain's length either, so watch the log during a rolling upgrade. A replica running a build from before chain preservation re-signs the CRL by writing one block, dropping every ancestor — and nothing detects it, because the next re-sign on an upgraded replica then reads one block and writes one block while
puppetca_crl_numberkeeps climbing. Upgrade every replica before importing a chain, and keep the imported bundle so it can be re-imported if this does happen. A re-sign that can see a difference says so, atWARN:level=WARN msg="Stored CRL chain length changed while re-signing" blocks_read=2 blocks_written=1On a healthy CA that line never appears: every block that is not ours is carried across, and import discards duplicates of our own. Treat one as a chain to inspect.
One series per known (non-deleted) leaf certificate or pending request. Cleaned
(puppet cert clean) certificates drop out of these series even though their
inventory line persists. The state label is one of requested (a pending CSR
with no issued certificate), signed, or revoked.
| Metric | Labels | Description |
|---|---|---|
puppetca_leaf_certificate_info |
subject, serial, state |
Constant 1. For requested, serial is empty. |
puppetca_leaf_certificate_not_before_timestamp_seconds |
subject, serial, state |
Issue time. Not emitted for requested. |
puppetca_leaf_certificate_not_after_timestamp_seconds |
subject, serial, state |
Expiry time. Not emitted for requested. |
puppetca_leaf_certificates |
state |
Count of leaf certificates per state. |
Expiry is not a
state: it is derived from thenot_aftertimestamp by alerting rules, so a certificate can be bothsigned/revokedand expired. To alert on expiry while ignoring revoked certs, filter onstate!="revoked", as the mixin does.
Only present when Kubernetes export targets are configured. Export failures are logged but never stop the CA, so these series are the way to alert on a target that persistently fails. One series per configured target (cardinality is bounded by the configuration).
| Metric | Labels | Description |
|---|---|---|
puppetca_k8s_export_applies_total |
kind, namespace, name, result |
Apply attempts per target; result is success or error. |
puppetca_k8s_export_last_success_timestamp_seconds |
kind, namespace, name |
Time of the last successful apply for each target. |
puppetca_k8s_export_last_error_timestamp_seconds |
kind, namespace, name |
Time of the last failed apply for each target. |
Exports are event-driven (startup and CRL updates) and can be days apart on a quiet CA, so alert by comparing
last_erroragainstlast_success(the mixin'sPuppetCAKubernetesExportFailingdoes this) rather than with rate windows or staleness thresholds, which misbehave between sparse attempts. A cycle that fails before any target is applied — the cert/CRL cannot be read from storage — touches none of these series, but storage failures already tripPuppetCAScrapeFailingviapuppetca_collector_scrape_success.
# CA certificate days until expiry
(puppetca_ca_certificate_not_after_timestamp_seconds - time()) / 86400
# Non-revoked leaf certificates expiring within 7 days
puppetca_leaf_certificate_not_after_timestamp_seconds{state!="revoked"} - time() < 7 * 86400
# Pending certificate requests
puppetca_leaf_certificate_info{state="requested"} == 1
# CA API error rate
sum(rate(puppetca_http_requests_total{code=~"5.."}[5m]))
# Kubernetes export targets whose most recent apply failed
puppetca_k8s_export_last_error_timestamp_seconds
> puppetca_k8s_export_last_success_timestamp_seconds
or
puppetca_k8s_export_last_error_timestamp_seconds
unless puppetca_k8s_export_last_success_timestamp_seconds
# Replicas enforcing a CRL behind the stored one (see below)
puppetca_crl_number - puppetca_crl_cached_number > 0
or
(puppetca_crl_cached_number unless puppetca_crl_number)
and on(instance) puppetca_collector_scrape_success == 1
The query above is how you confirm a revocation has reached the whole fleet. It
is normally empty, and briefly non-empty after each revocation while replicas
pick the new CRL up on their crl_sync_interval_sec timer. A replica that stays
in it is still admitting certificates the rest of the fleet has revoked.
The second arm matters as much as the first: a replica whose stored CRL cannot
be read or parsed publishes no puppetca_crl_number at all, so the subtraction
alone would go quiet in exactly the case worth paging on. It is qualified on a
successful scrape so that arm covers only the CRL being unreadable — a storage
outage drops the same series and is already paged by PuppetCAScrapeFailing.
This is the expression the mixin's PuppetCACRLStale rule uses, modulo its
target selector.
puppetca_crl_sync_failures_total usually says why a replica is stuck. If it is
flat, the reads are succeeding and the stored CRL genuinely has not advanced.
Restarting the replica reloads the CRL, selecting the newest block this CA signed the same way the sync does. If the stored chain carries none, startup warns loudly and keeps serving from whatever block 0 holds — an availability trade rather than a fail-closed refusal — while the re-sign paths refuse that blob outright. The sync then replaces the foreign copy as soon as a CRL of ours is stored. See Revocation across replicas.
See the mixin/ directory for the Jsonnet monitoring mixin and
instructions for rendering or importing it. It alerts on exporter availability,
CA/CRL/leaf expiry, pending requests, CRL update failures
(puppetca_crl_update_failures_total), a replica whose CRL has fallen behind
the stored one (puppetca_crl_cached_number,
puppetca_crl_sync_failures_total), and Kubernetes export failures, with all
thresholds configurable.