Skip to content

Instantly share code, notes, and snippets.

@ageis
Last active November 15, 2023 23:22
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ageis/63f599b040a6fac2b44c3b36f6aa8683 to your computer and use it in GitHub Desktop.
Save ageis/63f599b040a6fac2b44c3b36f6aa8683 to your computer and use it in GitHub Desktop.
certbot Prometheus exporter (Let's Encrypt metrics)

This is a script written in Python intended to run alongside a certbot instance and export statistics for monitoring purposes. It assumes the existence of certbot in the PATH plus read access to /etc/letsencrypt.

It tracks stuff like: number of certs, number of SANs, expiry time, seconds until expiry, and the status of the certificate per ACME.

How it works

Prometheus is a monitoring system and time-series database.

It works by pulling or scraping numerical metrics from an HTTP endpoint (or "exporter"), and then ingesting and keeping track of them over time. You can then build queries and alerting rules from this data.

An exporter set up as a scrape target may be local or remote. Prometheus is a great backend for a visualization and analytics software such as Grafana.

Testing and requirements

To see it in action, run certbot_exporter.py and navigate to http://127.0.0.1:8556 in your browser.

Ensure that prometheus_client is installed via pip.

Running as a service

I'd also recommend running this persistently as a systemd service. For example:

[Unit]
Description=certbot Prometheus exporter
After=network.target certbot.service

[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/certbot_exporter.py
KillMode=process
User=nobody
Group=nobody
Restart=on-failure

[Install]
WantedBy=multi-user.target
``
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Simple Prometheus exporter for Lets Encrypt metrics from a local certbot. """
import pytz
import re
import time
from datetime import timezone, datetime
from subprocess import check_output
import sys
from prometheus_client import start_http_server, Gauge, Counter, Enum, Summary, Info
CERTBOT_CERTS = Summary(
"certbot_certs", "Total number of certificates managed by Lets Encrypt"
)
CERTBOT_CERT_EXPIRY_SECONDS = Gauge(
"certbot_certs_expiry_seconds",
"Seconds until certificate expiry",
labelnames=["name", "domains"],
)
CERTBOT_CERT = Enum(
"certbot_cert",
"Status of certificate per ACME",
states=["UNKNOWN", "PENDING", "PROCESSING", "VALID", "INVALID", "REVOKED", "READY"],
labelnames=["name", "domains"],
)
CERTBOT_CERT_NAMES = Gauge(
"certbot_cert_names",
"Number of SANs (subject alternative names) in addition to the common name",
labelnames=["name"],
)
CERTBOT_CERT_STATUS = Info(
"certbot_cert_status", "Status of certificate per ACME", labelnames=["name"]
)
CERTBOT_CERT_EXPIRY_COUNTDOWN = Gauge(
"certbot_cert_expiry_countdown",
"Countdown—number of seconds until certificate expiry",
labelnames=["name"],
)
def query_certbot():
certbot = check_output(["certbot", "certificates"])
return certbot.decode("utf-8")
def main():
print(query_certbot())
certbot_output = query_certbot()
certificates = []
cert = {"name": None, "domains": None, "expiry": None}
certs = re.findall(
"Certificate Name: .*?Certificate Path:", certbot_output, flags=re.DOTALL
)
CERTBOT_CERTS.observe(len(certs))
for cert in certs:
name = cert.split("\n")[0].split(": ")[1]
domains = cert.split("\n")[1].split(": ")[1].split()
expiry_full = cert.split("Expiry Date: ", 1)[1]
expiry_status = expiry_full.split(" (")[1].split(":")[0]
expiry = expiry_full.split(" (")[0]
# 2019-06-11 14:21:19+00:00
datetime_object = datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S%z")
expiry_epoch = int(datetime_object.timestamp())
now = pytz.UTC.fromutc(datetime.utcnow())
expiring = expiry_epoch - now.timestamp()
CERTBOT_CERT_STATUS.labels(name=name).info({"state": expiry_status})
CERTBOT_CERT.labels(name=name, domains=domains).state(expiry_status)
CERTBOT_CERT_NAMES.labels(name=name).set(len(domains))
CERTBOT_CERT_EXPIRY_SECONDS.labels(name=name, domains=domains).set(expiry_epoch)
CERTBOT_CERT_EXPIRY_COUNTDOWN.labels(name=name).set(expiring)
start_http_server(8556)
while True:
time.sleep(3)
if __name__ == "__main__":
main()
@albix
Copy link

albix commented Oct 24, 2023

Thanks for sharing!

There is a little logic failure in the exporter script. The while loop in line 81 will never stop sleeping. Once certbot has rotated a certificate it will never report the new expiry date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment