Metadata-Version: 2.4
Name: certminder
Version: 0.7.0
Summary: Continuous TLS certificate monitoring and alerting, powered by certinspect.
Author-email: Michele Angrisano <michele.angrisano@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/mangrisano/certminder
Project-URL: Repository, https://github.com/mangrisano/certminder
Project-URL: Issues, https://github.com/mangrisano/certminder/issues
Keywords: tls,ssl,x509,certificate,monitoring,alerting,pki
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: certinspect>=1.9.1
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

<div align="center">

<img src="https://raw.githubusercontent.com/mangrisano/certminder/main/docs/logo.svg" alt="certminder" width="440">

[![CI](https://github.com/mangrisano/certminder/actions/workflows/ci.yml/badge.svg)](https://github.com/mangrisano/certminder/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/certminder?logo=pypi&logoColor=white&cacheSeconds=3600)](https://pypi.org/project/certminder/)
[![Python](https://img.shields.io/pypi/pyversions/certminder?logo=python&logoColor=white&cacheSeconds=3600)](https://pypi.org/project/certminder/)
[![Downloads](https://static.pepy.tech/badge/certminder)](https://pepy.tech/project/certminder)
[![License: MIT](https://img.shields.io/pypi/l/certminder?color=blue)](LICENSE)

**Scheduled checks · Expiry & revocation alerts · Fingerprint change detection · Deduplicated notifications · Console / email / Slack / webhook · Prometheus metrics**

[PyPI](https://pypi.org/project/certminder/) · [Quick start](#quick-start) · [Configure](#configure) · [Alerts](#what-it-alerts-on) · [Prometheus](#prometheus-metrics) · [Deployment](#deployment) · [Issues](https://github.com/mangrisano/certminder/issues)

</div>

**Continuous TLS certificate monitoring and alerting** — the watch loop on top
of [certinspect](https://github.com/mangrisano/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

```bash
pip install certminder       # pulls in certinspect automatically
# or from source:
pip install -e '.[dev]'
```

## Quick start

```bash
# 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`](certminder.example.yml)):

```yaml
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

Each certificate is inspected on every axis, so a certificate with several
faults raises **one alert per problem** (e.g. expired *and* an untrusted chain
give two separate events) — nothing is hidden behind a single headline status.
Every problem is deduplicated independently: it is notified once and clears with
its own `RECOVERED` event.

| 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)   |
| `WEAK_CRYPTO`          | warning  | small key or SHA-1/MD5 signature           |
| `CHAIN_EXPIRING`       | warning  | an intermediate/root CA is expired or near expiry |
| `FINGERPRINT_CHANGED`  | warning  | fingerprint differs from last cycle        |
| `UNREACHABLE`          | critical | host/handshake failed                      |
| `RECOVERED`            | info     | a specific 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 cycle
- `1` — at least one event was emitted
- `2` — 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:

```bash
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](https://github.com/prometheus/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/`](deploy/) plus a [`Dockerfile`](Dockerfile):

- **systemd timer** — [`certminder.service`](deploy/systemd/certminder.service) +
  [`certminder.timer`](deploy/systemd/certminder.timer) run one cycle on a
  schedule (cron-style, recommended).
- **systemd daemon** — [`certminder-daemon.service`](deploy/systemd/certminder-daemon.service)
  runs the `run` loop under supervision.
- **cron** — [`certminder.cron`](deploy/cron/certminder.cron) for hosts without
  systemd timers.
- **Docker** — multi-stage build; mount your `certminder.yml` at
  `/etc/certminder/certminder.yml` and a volume at `/var/lib/certminder`.

### Docker

Build the image:

```bash
docker build -t certminder .
```

Run a single cycle (cron-style — config and state mounted from the host):

```bash
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`):

```bash
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

```yaml
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:
```

```bash
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

```bash
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](https://github.com/mangrisano/certminder/issues) for bugs or ideas
- Send a pull request
- Share it with others who monitor TLS certificates

## License

MIT — see [LICENSE](LICENSE).
