Skip to content

Monitor expiring certificates

The most common first integration: get expiring certificates into whatever you already watch - Prometheus, a Slack channel, a weekly report.

Before building this, note that NextPKI already sends expiry alerts by email and raises expiry_30d through expiry_1d alerts internally. Build this when you need the data in your own system, not to reinvent the alerting.

There is no server-side expiry filter in v1. Page through and filter locally.

import os, urllib.request, json
from datetime import datetime, timezone, timedelta
BASE = "https://api.nextpki.com"
TOKEN = os.environ["NEXTPKI_TOKEN"]
def get(path):
req = urllib.request.Request(BASE + path,
headers={"Authorization": f"Bearer {TOKEN}"})
with urllib.request.urlopen(req) as r:
return json.load(r)
def all_certificates(page=200):
offset = 0
while True:
data = get(f"/v1/certificates?limit={page}&offset={offset}")
items = data["items"]
if not items:
return
yield from items
if len(items) < page:
return # short page means last page
offset += page
cutoff = datetime.now(timezone.utc) + timedelta(days=30)
expiring = [
c for c in all_certificates()
if datetime.fromisoformat(c["not_after"]) <= cutoff
and not c["is_revoked"]
]
expiring.sort(key=lambda c: c["not_after"])
for c in expiring:
days = (datetime.fromisoformat(c["not_after"]) - datetime.now(timezone.utc)).days
print(f"{days:>4}d {c['common_name'] or c['id']:<45} {c['issuer_dn'][:50]}")

Two details that matter. Stop paging on a short page, not on an empty one - otherwise you always make one wasted request. And exclude revoked certificates, or your report will nag about certificates that are already dead.

A naive “expires in under 30 days” list produces noise. Certificates worth waking someone for have three properties:

def actionable(c, seen_recently):
if c["is_revoked"]:
return False # already dead
if c["trust_status"] == "self_signed":
return False # an appliance, not a WebPKI cert
if not seen_recently:
return False # nothing serves it - clean up, don't renew
return True

The third condition is the one that turns a list into something useful, and it is also the one you cannot evaluate from the summary alone: fetch GET /v1/certificates/{id} and look at last_seen_at. A certificate not observed in weeks is a housekeeping task, not an outage risk.

Expose a gauge in seconds-until-expiry and let the alerting rules live where your other rules live.

def render_metrics(certs):
now = datetime.now(timezone.utc)
lines = [
"# HELP nextpki_certificate_expiry_seconds Seconds until certificate expiry.",
"# TYPE nextpki_certificate_expiry_seconds gauge",
]
for c in certs:
secs = int((datetime.fromisoformat(c["not_after"]) - now).total_seconds())
cn = (c["common_name"] or "").replace('"', '')
lines.append(
f'nextpki_certificate_expiry_seconds{{id="{c["id"]}",common_name="{cn}",'
f'issuer="{c["issuer_dn"][:60]}"}} {secs}'
)
return "\n".join(lines) + "\n"

Keep the label set small. One label per certificate attribute you might group by is tempting and turns into a cardinality problem on a large estate - id, common_name and a truncated issuer are enough.

Scrape your own exporter on your own interval, and refresh from NextPKI hourly. Certificate expiry does not change between scrapes; see rate limits.