You know that sinking feeling when a user calls to say your site is showing a scary browser warning? Or when your LDAP authentication silently dies at 3am because a certificate quietly expired while you were busy with other things – like sleeping?
check-certs is a certificate monitoring tool that watches expiry dates across all your servers and alerts you well before anything breaks. It checks all servers in parallel, verifies the full certificate chain (not just the leaf certificate – a broken intermediate CA is caught and reported), understands STARTTLS protocols like SMTP, IMAP and LDAP, and tracks state between runs so you only hear about something when it actually changes. Alerts go wherever you want them: a colour-coded terminal table for a quick glance, native macOS notifications, email, an HTTP webhook, Microsoft Teams, ntfy, or Pushover mobile push with emergency-priority acknowledgement for the ones that really can't wait.
No more surprise expirations. No more embarrassing phone calls. Just certificates, quietly minding their own deadlines.
- Overview
- Installation
- Server configuration
- Configuration
- Usage
- Single-server check
- Output
- Background monitoring
- Files
- Troubleshooting
- Contributing
- Licence
check-certs consists of check-certs.sh and the automation variants built on top of it.
check-certs.sh is the main script – a colour-coded terminal table that works on both macOS and Linux. It also contains the shared core logic that all automation variants build on. The installer always includes it.
Six optional automation variants extend it with background monitoring:
| Variant | Script | Platform | Details |
|---|---|---|---|
| Notification | check-certs-notify.sh |
macOS | Native notifications via launchd → docs/macos-notify.md |
check-certs-mail.sh |
Linux + macOS | Email via Postfix, ssmtp, or sendmail → docs/email.md | |
| Webhook | check-certs-webhook.sh |
Linux + macOS | HTTP POST to Slack, ntfy, Teams, custom endpoints → docs/webhook.md |
| Teams | check-certs-teams.sh |
Linux + macOS | Adaptive Card to Microsoft Teams via Workflow webhook → docs/teams.md |
| Pushover | check-certs-pushover.sh |
Linux + macOS | Mobile push with priority levels and emergency acknowledgement → docs/pushover.md |
| ntfy | check-certs-ntfy.sh |
Linux + macOS | Push notifications via ntfy.sh or self-hosted ntfy → docs/ntfy.md |
Key features:
- Checks all servers in parallel (up to
MAX_JOBSconcurrent connections, results displayed in originalservers.conforder) - Verifies the full certificate chain, not just the leaf certificate – a broken intermediate CA is caught and reported
- State tracking ensures you only get notified when something changes, not on every daily run
- Escalation levels with distinct behaviour at warning, critical and urgent thresholds
- STARTTLS auto-detected on standard ports (SMTP, IMAP, POP3, LDAP, FTP, XMPP)
Requires: Homebrew (coreutils and openssl are installed automatically).
Automatic – installs check-certs.sh, creates a symlink at /usr/local/bin/check-certs, and optionally configures one or more automation variants (notifications, email, webhook, Teams, Pushover, ntfy) via launchd:
chmod +x install/install.sh && ./install/install.shManual – terminal table only:
brew install coreutils openssl
sudo mkdir -p /usr/local/lib/check-certs
sudo cp src/check-certs.sh /usr/local/lib/check-certs/
sudo chmod +x /usr/local/lib/check-certs/check-certs.sh
sudo ln -s /usr/local/lib/check-certs/check-certs.sh /usr/local/bin/check-certs
mkdir -p ~/.config/check-certs
cp config/servers.conf config/check-certs.conf ~/.config/check-certs/To add background monitoring after a manual install, copy the relevant script from src/ to /usr/local/lib/check-certs/ and follow the setup guide:
- 🍎 macOS notifications –
check-certs-notify.sh - 📧 Email –
check-certs-mail.sh - 🌐 Webhook –
check-certs-webhook.sh - 💬 Teams –
check-certs-teams.sh - 📱 Pushover –
check-certs-pushover.sh - 🔔 ntfy –
check-certs-ntfy.sh
GNU date is available natively — no Homebrew or coreutils needed.
Automatic (Debian/Ubuntu) – installs check-certs.sh, creates a symlink at /usr/local/bin/check-certs, and optionally configures one or more automation variants (email, webhook, Teams, Pushover, ntfy) via cron:
chmod +x install/install.sh && sudo ./install/install.shManual – terminal table only:
apt install openssl # Debian/Ubuntu
# or: dnf install openssl # Fedora/RHEL
sudo mkdir -p /opt/check-certs
sudo cp src/check-certs.sh config/servers.conf config/check-certs.conf /opt/check-certs/
sudo chmod +x /opt/check-certs/check-certs.sh
sudo ln -s /opt/check-certs/check-certs.sh /usr/local/bin/check-certsTo add background monitoring after a manual install, copy the relevant script from src/ to your install directory and follow the setup guide:
- 📧 Email –
check-certs-mail.sh - 🌐 Webhook –
check-certs-webhook.sh - 💬 Teams –
check-certs-teams.sh - 📱 Pushover –
check-certs-pushover.sh - 🔔 ntfy –
check-certs-ntfy.sh
servers.conf is shared by all variants. Servers are organised into named groups:
# Lines starting with # are comments
[LDAP]
ldap.example.com:636:ldaps
ldap-plain.example.com:389
ldap-ng.example.com:389:ldap
[Mail]
mail.example.com:587:submission
imap.example.com:143:imap
imap.example.com:993:imaps
[Web]
www.example.com:443:https
intranet.example.com:443:https
[Services]
ticketing.example.com:443:https
custom.example.com:8443:tls
Entry format: hostname:port or hostname:port:proto
IPv6 addresses use bracket notation: [2001:db8::1]:443 or [::1]:636:ldaps
STARTTLS is automatically applied on standard ports. Optional key=value pairs after the port field override the global thresholds for that server:
| Override | Description |
|---|---|
warn=N |
Warning threshold in days |
crit=N |
Critical threshold in days |
urgent=N |
Urgent threshold in days (0 = disabled) |
timeout=N |
Connection timeout in seconds |
api.example.com:443 warn=30 crit=14 # stricter thresholds for a critical API
internal.example.com:443 warn=7 # more relaxed for internal tools
slow.example.com:443 timeout=15 # longer timeout for a slow host
check-certs --list shows active overrides next to each entry. Use the optional :proto
field to override protocol detection or to force plain TLS on a non-standard port.
| Port(s) | Auto-detected protocol |
|---|---|
| 25, 587 | smtp |
| 143 | imap |
| 110 | pop3 |
| 389 | ldap |
| 21 | ftp |
| 5222 | xmpp |
| all others | plain TLS |
STARTTLS protocols: smtp submission imap pop3 ldap ftp xmpp
Plain TLS aliases (self-documenting, no STARTTLS): tls https ldaps imaps pop3s smtps ftps
An existing
servers.confis never overwritten during reinstallation.
All settings live in check-certs.conf. The installer writes a minimal check-certs.conf containing only the settings relevant to the chosen variant. For a manual install, copy config/check-certs.conf from the repository as a starting point — it documents every available setting. To change any setting after installation, edit the file directly — the scripts themselves never need to be modified.
nano ~/.config/check-certs/check-certs.conf # macOS
nano /opt/check-certs/check-certs.conf # LinuxKey settings:
| Setting | Default | Description |
|---|---|---|
WARN_DAYS |
15 |
First warning X days before expiry |
CRIT_DAYS |
7 |
Daily reminder from X days before expiry |
URGENT_DAYS |
2 |
Urgent alert from X days (0 = disabled) |
TIMEOUT |
5 |
Connection timeout per server in seconds |
MAX_JOBS |
10 |
Maximum parallel checks |
MAIL_TRANSPORT |
postfix (Linux) / ssmtp (macOS) |
Email transport: postfix, ssmtp, or sendmail |
MAIL_TO |
– | Primary email recipient |
MAIL_TO_URGENT |
– | Second recipient for urgent alerts |
MAIL_FROM |
– | Sender address |
WEBHOOK_URL |
– | URL to POST findings to |
TEAMS_WEBHOOK_URL |
– | Teams Workflow webhook URL |
PUSHOVER_APP_TOKEN |
– | Pushover application token |
PUSHOVER_USER_KEY |
– | Pushover user or group key |
NTFY_URL |
– | ntfy server URL (e.g. https://ntfy.sh) |
NTFY_TOPIC |
– | ntfy topic name |
NTFY_TOKEN |
– | ntfy access token (optional, for protected topics) |
On reinstallation, an existing
check-certs.confis backed up tocheck-certs.conf.bakbefore being overwritten.
check-certs # Check all servers from servers.conf
check-certs <hostname> # Check a single server (port defaults to 443)
check-certs <hostname>:<port> # Check a single server on a specific port
check-certs <hostname>:<port>:<proto> # Check with explicit STARTTLS protocol
check-certs --scan <hostname> # Probe common TLS ports (onboarding helper)
check-certs --list # List all servers without running checks
check-certs --check # key=value for all servers in servers.conf
check-certs --check <host>[:<port>] # key=value for a single host (port defaults to 443)
check-certs --check <host1> <host2> … # key=value batch mode (multiple hosts, parallel)
check-certs --check --nagios <host>[:<port>] … # Nagios/Icinga output, one line per host
check-certs --check --json # JSON array for all servers
check-certs --check --json <host>[:<port>] # JSON object for a single host
check-certs --check --json <host1> <host2> … # JSON array for multiple hosts
check-certs --clear-state # Clear all state (forces fresh notifications)
check-certs --version # Show version
check-certs --help # Show helpcheck-certs --check outputs machine-readable certificate data — useful for
scripting, monitoring integrations, and testing STARTTLS configuration.
Without arguments it checks every server in servers.conf. With a single
hostspec it checks that one server (port defaults to 443). With multiple
hostspecs it runs them in parallel (batch mode). IPv6: [::1]:443.
Output modes:
check-certs --check # kv for all servers in servers.conf
check-certs --check --json # JSON array for all servers
check-certs --check mail.example.com # kv, single host, port defaults to 443
check-certs --check mail.example.com:587 # kv, explicit port
check-certs --check api.example.com ldap.example.com:636 # batch mode, two hosts in parallel
check-certs --check --nagios mail.example.com:587 # Nagios/Icinga output
check-certs --check --json mail.example.com:587 # JSON object
check-certs --check --json api.example.com ldap.example.com:636 # JSON arraykey=value output — one field per line, parse by key name not position:
host=mail.example.com
port=587
proto=smtp
days=12
expiry=Jun 01 2026
expiry_ts=1748736000
ca=Let's Encrypt
status=WARNING
chain=OK
| Field | Description |
|---|---|
host |
Hostname as given |
port |
Port checked |
proto |
STARTTLS protocol used (smtp, ldap, …) or tls for plain TLS |
status |
OK, WARNING, CRITICAL, URGENT, EXPIRED, or ERROR |
days |
Days until expiry (negative if already expired) |
expiry |
Expiry date, human-readable (Mon DD YYYY) |
expiry_ts |
Expiry date as a Unix timestamp |
ca |
Certificate issuer name |
chain |
OK or a chain verification error message |
On ERROR (unreachable or invalid port), only host, port, proto, status, and reason are printed.
Exit codes:
| Code | Meaning |
|---|---|
0 |
OK (certificate valid, chain OK) |
1 |
WARNING |
2 |
CRITICAL, URGENT, EXPIRED, or ERROR |
3 |
UNKNOWN — unreachable host (--nagios mode only) |
Scripting examples:
# Branch on exit code
if ! check-certs --check api.example.com; then
echo "Certificate issue on api.example.com"
fi
# Parse a specific field
days=$(check-certs --check cert.example.com | grep "^days=" | cut -d= -f2)
[ "$days" -lt 14 ] && send_alert "Certificate expiring in ${days}d"
# Compare expiry timestamps across hosts
check-certs --check a.example.com | grep "^expiry_ts="
check-certs --check b.example.com | grep "^expiry_ts="
# Use as a Nagios/Icinga plugin
check-certs --check --nagios monitor.example.com:443
# Feed into a dashboard or log pipeline
check-certs --check --json api.example.com:443Colour-coded table in the terminal, grouped by sections from servers.conf:
╔══════════════════════════════════╦════════════════════╦════════════════╦════════════════════════╦═════╗
║ Server ║ Expiry date ║ Remaining ║ Issued by ║ Ch ║
╠══════════════════════════════════╬════════════════════╬════════════════╬════════════════════════╬═════╣
╠ LDAP ══════════════════════════════════════════════════════════════════════════════════════════════╣
║ ldap.example.com ║ Nov 20 2026 ║ ✓ 185d ║ R11 ║ ✓ ║
║ ldap-dev.example.com ║ - ║ ERROR ║ Unreachable ║ ║
╠══════════════════════════════════╬════════════════════╬════════════════╬════════════════════════╬═════╣
╠ Web ═══════════════════════════════════════════════════════════════════════════════════════════════╣
║ www.example.com ║ Jul 14 2026 ║ ⚠ 28d ║ GEANT TLS RSA 1 ║ ✓ ║
║ intranet.example.com ║ Jun 01 2026 ║ ✗ 14d ║ GEANT TLS RSA 1 ║ ⚠ ║
╚══════════════════════════════════╩════════════════════╩════════════════╩════════════════════════╩═════╝
Summary: 4 servers checked │ ✓ 1 OK │ ⚠ 1 Warning │ ✗ 2 Critical/Error
| Colour | Condition | Meaning |
|---|---|---|
| 🟢 Green | ≥ WARN_DAYS remaining |
All good |
| 🟡 Yellow | < WARN_DAYS remaining |
Renew soon |
| 🔴 Red | < CRIT_DAYS remaining |
Immediate action required |
| 🔴 Red / ERROR | – | Server unreachable |
The "Issued by" column shows the CN value from the certificate issuer (e.g. R11 for Let's Encrypt, GEANT TLS RSA 1 for GÉANT), falling back to the O value if CN is absent. The Ch column shows ✓ when the full certificate chain is valid, or ⚠ when an intermediate CA is missing or invalid. A broken chain promotes an otherwise-OK certificate to CRITICAL — the chain_status field in --check output carries the reason string.
Once you have check-certs.sh set up, you can add automated background monitoring. All variants use the same servers.conf and check-certs.conf, notify only when state changes, and escalate through warning → critical → urgent levels with daily reminders for unresolved issues.
- 🍎 macOS notifications – daily launchd job, native macOS notifications with escalation levels
- 📧 Email – daily email reports via Postfix, ssmtp, or sendmail (Linux and macOS)
- 🌐 Webhook – HTTP POST to Slack, ntfy.sh, Teams, Mattermost, or any custom endpoint
- 💬 Teams – full Adaptive Card to a Microsoft Teams channel via Workflow webhook
- 📱 Pushover – mobile push notifications with emergency acknowledgement for iOS and Android
- 🔔 ntfy – push notifications via ntfy.sh or a self-hosted ntfy server
- 🔧 Build your own wrapper – full interface reference for custom delivery scripts
README.md
README-DE.md
CHANGELOG.md
CONTRIBUTING.md
LICENSE
docs/
├── macos-notify.md ← macOS notification variant
├── email.md ← Email variant (Postfix, ssmtp, or sendmail)
├── webhook.md ← Webhook variant
├── teams.md ← Microsoft Teams Adaptive Card variant
├── pushover.md ← Pushover variant
├── ntfy.md ← ntfy variant
├── wrapper-interface.md ← Interface reference for building custom wrappers
└── troubleshooting.md ← Troubleshooting for all platforms
src/
├── check-certs.sh ← Main script – terminal table + core logic
├── check-certs-notify.sh ← macOS notification variant
├── check-certs-mail.sh ← Email variant
├── check-certs-webhook.sh ← Webhook variant
├── check-certs-teams.sh ← Teams variant
├── check-certs-pushover.sh ← Pushover variant
└── check-certs-ntfy.sh ← ntfy variant
install/
├── install.sh ← Unified installer (macOS and Linux)
├── cleanup-macos.sh ← Cleanup script for pre-2.7.0 installations
├── com.check-certs.notify.plist ← launchd job template (notifications)
├── com.check-certs.mail.plist ← launchd job template (email)
├── com.check-certs.webhook.plist ← launchd job template (webhook)
├── com.check-certs.teams.plist ← launchd job template (Teams)
├── com.check-certs.pushover.plist ← launchd job template (Pushover)
├── com.check-certs.ntfy.plist ← launchd job template (ntfy)
└── check-certs.logrotate ← logrotate config (Linux)
config/
├── servers.conf ← Example server list
└── check-certs.conf ← Configuration file (all settings documented)
tests/
└── test_check_certs.sh ← Unit test suite (no network required)
| Error | Solution |
|---|---|
check-certs.sh not found |
Script is not in the same directory as the calling wrapper |
| "Server file not found" | Check SERVER_FILE in check-certs.conf or verify servers.conf exists |
| "Unreachable" | openssl s_client -connect hostname:port </dev/null |
| "Invalid format" | Separator in servers.conf must be :, not , |
| CA shows "Unknown" | openssl s_client -connect hostname:port </dev/null 2>/dev/null | openssl x509 -noout -issuer |
| Chain always invalid | Usually a missing intermediate CA in the local trust store. Update: brew install ca-certificates (macOS) or apt install ca-certificates (Linux). Verify with: openssl s_client -connect hostname:port -servername hostname </dev/null |
| "gdate: command not found" | macOS only: brew install coreutils |
| "Homebrew not found" | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
check-certs command not found |
macOS: check the symlink: ls -la /usr/local/bin/check-certs. Linux: run source ~/.bashrc or open a new terminal. |
For further troubleshooting and variant-specific issues see docs/troubleshooting.md.
Contributions are welcome. Please open an issue before starting work on a larger feature so we can discuss the approach. For bug fixes, a pull request with a clear description of the problem and fix is sufficient.
MIT – see LICENSE for the full text.