Scheduled checks · Expiry & revocation alerts · Fingerprint change detection · Deduplicated notifications · Console / email / Slack / webhook · Prometheus metrics
PyPI · Quick start · Configure · Alerts · Prometheus · Deployment · Issues
Continuous TLS certificate monitoring and alerting — the watch loop on top of certinspect.
certinspect tells you what a certificate looks like right now.
certminder runs it on a schedule, remembers what it saw last time, and
alerts you when a certificate is about to expire, gets revoked, changes
fingerprint, or becomes unreachable.
Why a separate tool
certminder never re-implements TLS or X.509 logic — that all lives in certinspect. certminder adds only what a monitor needs:
- a schedule (run once for cron, or loop as a daemon),
- state memory to detect changes between runs,
- deduplicated alerts (notify once per condition, recover once),
- pluggable notifiers (console, email, Slack, generic webhook),
- optional Prometheus metrics for the node_exporter textfile collector.
Install
pip install certminder # pulls in certinspect automatically
# or from source:
pip install -e '.[dev]'
Quick start
# inspect a single host ad hoc
certminder check example.com
# copy and edit the sample config, then:
certminder once -c certminder.yml # one cycle — ideal for cron
certminder run -c certminder.yml # run continuously as a daemon
Configure
Everything is driven by a YAML file (see
certminder.example.yml):
interval: 6h
state_file: ~/.certminder/state.json
defaults:
verify: true
days: 30
critical_days: 15
notifiers:
- type: console
- type: slack
webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
- type: email
host: smtp.example.com
port: 587
username: alerts@example.com
password: CHANGE_ME
from_addr: alerts@example.com
to: [ops@example.com]
targets:
- host: example.com
- host: api.example.com
port: 8443
- host: mail.example.com
starttls: smtp
- host: short-lived.example.com
cab_forum: true # fail if validity exceeds today's CA/Browser Forum cap
- host: hardened.example.com
require_sct: true # require Certificate Transparency SCTs
require_must_staple: true # require the OCSP Must-Staple extension
min_tls_version: TLSv1.2 # require at least TLS 1.2
- host: strict.example.com
profile: strict # one-flag hardening bundle (lenient/standard/strict)
The opt-in policy checks (all raise POLICY_VIOLATION) are: cab_forum or
not_after_max (maximum validity), require_sct (Certificate Transparency),
require_must_staple (OCSP Must-Staple), and min_tls_version (minimum
negotiated TLS version). cab_forum and not_after_max are mutually
exclusive. A profile (lenient, standard or strict) applies a named
bundle of these checks in one line; any explicit check above overrides it.
What it alerts on
| Event | Severity | Trigger |
|---|---|---|
EXPIRING |
warning | within --days of expiry |
CRITICAL / EXPIRED |
critical | within critical_days, or already expired |
NOT_YET_VALID |
critical | validity period starts in the future |
REVOKED |
critical | OCSP/CRL says revoked (needs verify) |
CHAIN_UNTRUSTED |
critical | chain fails to validate |
HOSTNAME_MISMATCH |
critical | cert does not match the hostname |
POLICY_VIOLATION |
critical | fails an opt-in policy check (see below) |
FINGERPRINT_CHANGED |
warning | fingerprint differs from last cycle |
UNREACHABLE |
critical | host/handshake failed |
RECOVERED |
info | a prior problem cleared |
Each condition alerts once; certminder remembers it and stays quiet until it changes, then sends a single recovery notice.
Exit codes (once)
0— no events this cycle1— at least one event was emitted2— configuration error
Add --json to once to print a machine-readable summary of the cycle (one
entry per target plus the events) to stdout, handy for piping:
certminder once -c certminder.yml --json | jq '.targets[] | {target, status, days_to_expire}'
Prometheus metrics
Set prometheus_file in the config to a path inside the node_exporter
textfile collector
directory. certminder rewrites it atomically at the end of every cycle:
certminder_certificate_expiry_days{target="example.com:443",host="example.com",port="443",status="VALID"} 42
certminder_certificate_valid{...} 1
certminder_target_up{...} 1
certminder_last_run_timestamp_seconds 1700000000
Deployment
Ready-to-use units live in deploy/ plus a Dockerfile:
- systemd timer —
certminder.service+certminder.timerrun one cycle on a schedule (cron-style, recommended). - systemd daemon —
certminder-daemon.serviceruns therunloop under supervision. - cron —
certminder.cronfor hosts without systemd timers. - Docker — multi-stage build; mount your
certminder.ymlat/etc/certminder/certminder.ymland a volume at/var/lib/certminder.
Docker
Build the image:
docker build -t certminder .
Run a single cycle (cron-style — config and state mounted from the host):
docker run --rm \
-v "$PWD/certminder.yml:/etc/certminder/certminder.yml:ro" \
-v certminder-state:/var/lib/certminder \
certminder once -c /etc/certminder/certminder.yml
Run continuously as a daemon (this is the default CMD):
docker run -d --name certminder \
--restart unless-stopped \
-v "$PWD/certminder.yml:/etc/certminder/certminder.yml:ro" \
-v certminder-state:/var/lib/certminder \
certminder
The named volume certminder-state persists state.json and the Prometheus
file across restarts — keep it so deduplication survives container recreation.
The console notifier prints to stdout; read it with docker logs -f certminder
(timestamps from Docker with -t, or set timestamp: true on the console
notifier). The container runs in UTC.
Docker Compose
services:
certminder:
build: . # or: image: certminder
container_name: certminder
restart: unless-stopped
command: run -c /etc/certminder/certminder.yml
volumes:
- ./certminder.yml:/etc/certminder/certminder.yml:ro
- certminder-state:/var/lib/certminder
logging: # cap the daemon's logs so they don't grow without bound
driver: json-file
options:
max-size: "10m"
max-file: "5"
volumes:
certminder-state:
docker compose up -d # build (if needed) and start the daemon
docker compose logs -f certminder
docker compose up -d --build # rebuild after upgrading certminder/certinspect
docker compose down # stop and remove
Development
ruff check . && ruff format --check .
pytest -q
Tests mock the certinspect subprocess, so the suite never touches the network.
Support
If certminder is useful to you, the best ways to support it are:
- Star the repo to help others discover it
- Open an issue for bugs or ideas
- Send a pull request
- Share it with others who monitor TLS certificates
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file certminder-0.6.0.tar.gz.
File metadata
- Download URL: certminder-0.6.0.tar.gz
- Upload date:
- Size: 27.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce4f694fa7752cf61dc8c5087b3719cce65d376ad39841fdfdf62c3252ea4779
|
|
| MD5 |
f7bc864dc5c6f37a8de5f423bba4a6c7
|
|
| BLAKE2b-256 |
e9e631e52d2b9b4788dd652d648110d971e2dd9b20159a0c2d27cb47b169e0ad
|
Provenance
The following attestation bundles were made for certminder-0.6.0.tar.gz:
Publisher:
publish.yml on mangrisano/certminder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
certminder-0.6.0.tar.gz -
Subject digest:
ce4f694fa7752cf61dc8c5087b3719cce65d376ad39841fdfdf62c3252ea4779 - Sigstore transparency entry: 2280317708
- Sigstore integration time:
-
Permalink:
mangrisano/certminder@64f1683399c5368ebfa0b1a212fe7aec83efe64a -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/mangrisano
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@64f1683399c5368ebfa0b1a212fe7aec83efe64a -
Trigger Event:
push
-
Statement type:
File details
Details for the file certminder-0.6.0-py3-none-any.whl.
File metadata
- Download URL: certminder-0.6.0-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f89099fbc8ce71dc4238b1d0cbee2e737d56fec25aba98287c47f2beb10bb71a
|
|
| MD5 |
844afdac0823b40ca7064d14f5f47d82
|
|
| BLAKE2b-256 |
b34beb424bc266be44bc28c91c4b9bc92dbe87d2b4e59c918acf34ea7645f4bf
|
Provenance
The following attestation bundles were made for certminder-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on mangrisano/certminder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
certminder-0.6.0-py3-none-any.whl -
Subject digest:
f89099fbc8ce71dc4238b1d0cbee2e737d56fec25aba98287c47f2beb10bb71a - Sigstore transparency entry: 2280317717
- Sigstore integration time:
-
Permalink:
mangrisano/certminder@64f1683399c5368ebfa0b1a212fe7aec83efe64a -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/mangrisano
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@64f1683399c5368ebfa0b1a212fe7aec83efe64a -
Trigger Event:
push
-
Statement type: