Skip to content

Instantly share code, notes, and snippets.

@jcefoli
Created December 28, 2025 18:35
Show Gist options
  • Select an option

  • Save jcefoli/feb971de73d44018c52aaba0d791d4c7 to your computer and use it in GitHub Desktop.

Select an option

Save jcefoli/feb971de73d44018c52aaba0d791d4c7 to your computer and use it in GitHub Desktop.
Certbot Expiry Checker
import json
import re
import subprocess
from datetime import datetime, timezone
_FIELD_REGEX = re.compile(r"^\s*([^:]+):\s*(.+)$")
def _run_certbot():
command = ["sudo", "certbot", "certificates"]
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError as exc:
stderr = exc.stderr.strip() or exc.stdout.strip() or "no output captured"
raise RuntimeError(
f"certbot command failed (exit code {exc.returncode}): {stderr}"
) from exc
return result.stdout
def _field_value(line, key):
match = _FIELD_REGEX.match(line)
if match and match.group(1).strip() == key:
return match.group(2).strip()
return None
def _parse_expiry(raw_value):
timestamp = raw_value.split(" (", 1)[0].strip()
return datetime.fromisoformat(timestamp)
def get_certbot_certs():
output = _run_certbot()
certs = []
cert_data = None
for line in output.splitlines():
name_value = _field_value(line, "Certificate Name")
if name_value:
if cert_data:
certs.append(cert_data)
cert_data = {"name": name_value, "domains": []}
continue
if not cert_data:
continue
domains_value = _field_value(line, "Domains")
if domains_value is not None:
cert_data["domains"] = domains_value.split()
continue
expiry_value = _field_value(line, "Expiry Date")
if expiry_value is not None:
try:
expiry_dt = _parse_expiry(expiry_value)
now = datetime.now(expiry_dt.tzinfo or timezone.utc)
cert_data["expiry"] = expiry_dt.isoformat()
cert_data["days_remaining"] = (expiry_dt - now).days
cert_data["_expiry_dt"] = expiry_dt
except ValueError:
cert_data["expiry"] = expiry_value
continue
cert_path_value = _field_value(line, "Certificate Path")
if cert_path_value is not None:
cert_data["cert_path"] = cert_path_value
continue
key_path_value = _field_value(line, "Private Key Path")
if key_path_value is not None:
cert_data["key_path"] = key_path_value
if cert_data:
certs.append(cert_data)
certs.sort(key=lambda c: c.get("_expiry_dt", datetime.max))
for cert in certs:
cert.pop("_expiry_dt", None)
return certs
if __name__ == "__main__":
certs_json = json.dumps(get_certbot_certs(), indent=4)
print(certs_json)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment