Check cert¶
Overview¶
Inspects X.509 certificates and alerts on days remaining until expiry, hostname mismatch and chain verification failures. Three sources via --source: url connects to a single TLS endpoint, runs a TLS handshake and captures the server certificate; file reads one or many local certificate files, with glob expansion for batch monitoring of cert directories; scan discovers the hosts of a subnet (the default interface’s subnet, a chosen interface, an explicit network in CIDR notation, or an explicit host list) and probes each host on the ports given by --ports, inspecting every certificate it gets. With --source=url the plugin only runs the TLS handshake and reads the server certificate (no HTTP request is sent), so it works for any „TLS from start“ service - HTTPS, IMAPS, LDAPS, SMTPS, AMQPS, MQTTS, custom TLS ports. STARTTLS-style upgrades on plaintext ports (SMTP 587, IMAP 143, LDAP 389) are not supported. The default source is scan, so without any parameter the plugin scans the default interface’s subnet on a set of common data-center TLS ports (HTTPS, mail, LDAPS, AMQPS, MQTTS and common management interfaces; see the --ports help for the full default list). p12 and jks sources are reserved for future expansion without renaming the plugin or breaking existing service templates.
Important Notes:
--source=url: Hostname mismatch and chain verification failures share one--severity(default WARN), so operators running internal CAs are not paged for trust issues that are expected in their environment. Set--severity=critto enforce strict trust.--source=file: chain and hostname checks are not performed. Only days remaining is evaluated. With a glob that matches many files, the worst state across all matches drives the plugin state. The File column abbreviates each path zsh-style (/etc/ssl/certs/web1.pembecomes/e/s/c/web1.pem) so wide cert-directory listings stay readable; the full path is shown in--lengthyoutput.--source=scan: each reachable certificate is checked for expiry and for chain/trust (does it chain to a CA in the system trust store, or one added via--ca-file?). Hostname verification is not done, because a subnet scan reaches IP addresses whose certificates legitimately do not match. A valid self-signed certificate is tolerated (it is cryptographically as sound as a publicly trusted one); other trust failures (unknown CA, expired or broken chain) raise the state via--severity(default WARN,critto enforce).--insecureturns trust verification off entirely for pure expiry monitoring. Counting follows thelynischeck and is done per host, not per host/port: a/24reportsX/254 hosts responded, notX/508 targets. Hosts (or host/port combinations) that do not answer within--timeoutare skipped silently, so an empty subnet returns OK with0/N hosts responded. The worst state across all reachable certificates drives the plugin state. Scan a large subnet from a host that can actually reach it, and tune--max-workers(parallelism) and--timeoutso the run finishes within the check interval.--source=url: the plugin inspects the full certificate chain the server sends, not just the leaf. The leaf carries the chain/hostname verdict; every intermediate is additionally checked for expiry, so a soon-to-expire intermediate raises the state and shows up as the soonest-expiring certificate in the output. Capturing the chain requires Python 3.13 or newer (ssl.SSLSocket.get_unverified_chain()); on older Python only the leaf is inspected. The chain is shown one block per certificate in--lengthy.--warningand--criticalaccept three forms: a Nagios range in days (14:), a percentage of the certificate’s total validity period (25%, alert when less than 25% of the lifetime is left), or a duration with a unit (14d,12h,2W,1M). The percentage form matches the convention of other X.509 scanners and adapts to short-lived (90-day) and long-lived certificates alike.Expired certificates are unconditionally reported as CRIT, regardless of the
--warningand--criticalthresholds.--insecureskips chain (and, for--source=url, hostname) verification entirely. The certificate is still fetched and inspected for expiry, the chain verdict is then reported as „verification skipped“. Use it for pure expiry monitoring when trust is verified elsewhere or not relevant.When a PEM file contains multiple certificates (for example
fullchain.pem, a CA bundle, or a chain file), each certificate becomes its own item in the output and is checked independently. So afullchain.pemwith leaf plus one intermediate produces two rows in the table; the worst state across all of them drives the plugin state. To check only the leaf, point--filenameat a single-cert file likecert.pem.What „chain verified“ actually covers: the leaf chains to a trust anchor in the system trust store (or
--ca-file) using the intermediates the server sent or that the local trust store has cached, every certificate in the verified path is within itsnotBefore/notAfterwindow, every signature in that path is cryptographically valid, and the value of--sni-hostname(or, when not set, the host part of--url) appears in the leaf certificate’ssubjectAltName(orCommonNamefor legacy certs). What it does not cover: OCSP responder lookups, CRL checks, Certificate Transparency / SCT validation, enforcement of the order in which the server sends the chain, detection of SHA-1 in the verified chain, and the completeness of the chain the server sent (a server that omits intermediates may still verify locally if they are cached, but break for clients with empty caches). For those compliance-style checks use a dedicated TLS scanner likesslyze.
Data Collection:
--source=url: opens a TCP connection to the host and port from--url, runs a TLS handshake and reads the certificate chain the server presents (leaf plus intermediates on Python 3.13+, leaf only on older Python). No HTTP request is sent. The chain is verified against the system trust store;--ca-fileadds one or more CA bundles to the trusted set and can be given multiple times.--sni-hostnameoverrides the SNI value sent during the handshake;--client-certand--client-keyattach a client certificate for mutual TLS.--source=file: reads each file matching--filename(PEM or DER, autodetected) and parses every certificate found. PEM bundles expand to one item per certificate.--filenamesupports glob (*,?,[abc]) and recursive glob (**). When the glob matches files that don’t look like certificates (private keys, plain text), they are silently skipped so recursive scans of/etc/ssl/**are safe. Always quote the glob pattern, otherwise the shell expands it before the plugin sees it and only the first match reaches--filename.--source=scan: enumerates the target hosts (--hostoverrides discovery, otherwise--networkin CIDR notation, otherwise--interface, otherwise the default interface’s subnet;--excludedrops individual addresses or names), then probes each host in parallel (--max-workers) on every port from--ports(each value a single port like443or a range like8000-8100). For each reachable port it runs a TLS handshake, verifies the chain against the system trust store plus any--ca-file(without hostname checking), and evaluates the certificate’s expiry.--client-certand--client-keyattach a client certificate for mutual TLS. Auto-discovery via--interfaceor the default interface requires thepsutilPython module.
Fact Sheet¶
Fact |
Value |
|---|---|
Check Plugin Download |
https://github.com/Linuxfabrik/monitoring-plugins/tree/main/check-plugins/cert |
Nagios/Icinga Check Name |
|
Check Interval Recommendation |
Every day |
Can be called without parameters |
Yes (scans the default interface’s subnet on common TLS ports) |
Runs on |
Cross-platform |
Compiled for Windows |
No (runs with Python interpreter) |
3rd Party Python modules |
|
Help¶
usage: cert [-h] [-V] [--always-ok] [--ca-file CA_FILE]
[--client-cert CLIENT_CERT] [--client-key CLIENT_KEY] [-c CRIT]
[--exclude EXCLUDE] [--filename FILENAME] [-H HOST] [--insecure]
[--interface INTERFACE] [--lengthy] [--max-workers MAX_WORKERS]
[--network NETWORK] [--ports PORTS] [--severity {crit,warn}]
[--sni-hostname SNI_HOSTNAME] [--source {file,scan,url}]
[--timeout TIMEOUT] [--url URL] [--verbose] [-w WARN]
Inspects X.509 certificates and alerts on days remaining until expiry,
hostname mismatch and chain verification failures. Sources via --source: `url`
fetches the certificate from a single TLS endpoint and verifies the chain
against the system trust store by default (override with --ca-file); `file`
reads one or many certificate files via glob expansion (PEM or DER); `scan`
discovers the hosts of a subnet (the subnet of the default interface, a chosen
interface, an explicit network in CIDR notation, or an explicit host list),
connects to each one on the ports given by --ports and inspects every
certificate it gets. PEM bundles expand to one item per certificate, so a
fullchain.pem produces a row for the leaf and a row for each intermediate.
With --source url the plugin only runs the TLS handshake and reads the server
certificate; no HTTP request is sent. That means it works for any "TLS from
start" service, not only HTTPS: IMAPS (port 993), LDAPS (636), SMTPS (465),
AMQPS (5671), MQTTS (8883), custom TLS ports - just point --url at the right
host and port (`https://mail.example.com:993/` inspects the IMAPS cert).
STARTTLS protocols that upgrade an existing plaintext connection (SMTP
submission on 587, IMAP on 143, LDAP on 389) are not supported. Hostname
verification only applies to --source url; chain/trust verification applies to
--source url and --source scan and its failures use --severity (warn or crit),
while a valid self-signed certificate is tolerated. Expired certificates are
unconditionally reported as CRIT. With --source file and --source scan the
worst state across all inspected certificates drives the plugin state; targets
that do not answer within --timeout are skipped. The default source is `scan`,
so without any parameter the plugin scans the default interface's subnet on a
set of common data-center TLS ports (HTTPS, mail, LDAPS, AMQPS, MQTTS and
common management interfaces); see --ports for the full default list.
options:
-h, --help show this help message and exit
-V, --version show program's version number and exit
--always-ok Always returns OK.
--ca-file CA_FILE Path to a CA bundle in PEM format, trusted for chain
verification in addition to the system trust store.
Can be specified multiple times to combine several
bundles. Applies to --source=url and --source=scan.
Example: `--ca-file=/etc/pki/ca-
trust/source/anchors/internal.pem`
--client-cert CLIENT_CERT
Path to a client certificate in PEM format for mutual
TLS.
--client-key CLIENT_KEY
Path to the client certificate private key in PEM
format.
-c, --critical CRIT CRIT threshold for the time remaining until the
certificate expires. Accepts a Nagios range in days
(`5:`), a percentage of the total validity period
(`10%`, CRIT when less than 10% of the lifetime is
left), or a duration with a unit (`3d`, `12h`, `2W`,
`1M`; CRIT when less time than that is left).
Examples: `5:` `10%` `3d`. Default: 5:
--exclude EXCLUDE IP address or hostname to skip during a scan. Matched
against both the discovered target address and, for
--host, the given name. Only applies to --source=scan.
Can be specified multiple times. Example:
`--exclude=192.0.2.1 --exclude=192.0.2.254`
--filename FILENAME Path to a certificate file or a glob pattern matching
multiple certificate files. Required when
--source=file. Files are read as PEM or DER
(autodetected); when a PEM bundle contains multiple
certificates (typical for fullchain.pem or a CA
bundle), each certificate becomes its own row in the
output. Globs follow Python conventions: `*` matches
one path segment, `**` matches across directories.
Always quote the pattern in shells so that the shell
does not expand the wildcard before the plugin sees
it. Example: `--filename='/etc/ssl/certs/*.pem'`.
Recursive example:
`--filename='/etc/letsencrypt/live/**/cert.pem'`
-H, --host HOST Target host to scan. Overrides subnet auto-discovery.
Only applies to --source=scan. Can be specified
multiple times. If not specified, the subnet of the
default interface is scanned. Example:
`--host=mail.example.com --host=192.0.2.10`
--insecure Skip chain and hostname verification entirely. The
certificate is still fetched and inspected, but the
chain verdict is reported as "verification skipped".
--interface INTERFACE
Network interface whose subnet is scanned. Only
applies to --source=scan. Ignored when --host or
--network is given. If not specified, the default
interface (the one carrying the default route) is
used.
--lengthy Extended reporting.
--max-workers MAX_WORKERS
Maximum number of targets to scan in parallel. Only
applies to --source=scan. Default: 10
--network NETWORK Network in CIDR notation to scan for targets via auto-
discovery. Only applies to --source=scan. Takes
precedence over --interface. Can be specified multiple
times. Example: `--network=192.0.2.0/24`
--ports PORTS TCP port to probe on every scanned target. A range is
written `start-end`. Only applies to --source=scan.
Can be specified multiple times. If not specified, a
set of common data-center TLS ports is probed (443,
465, 636, 990, 993, 995, 3269, 5671, 5986, 6443, 8006,
8200, 8443, 8883, 9090, 9443, 10000). Example:
`--ports=443 --ports=993 --ports=8000-8100`
--severity {crit,warn}
Severity assigned to chain/trust verification failures
(--source=url and --source=scan) and to hostname
mismatches (--source=url only). A valid self-signed
certificate is always tolerated. Defaults to warn so
that operators running internal CAs are not paged by
trust issues that are expected in their environment.
Set to `crit` to enforce strict trust.
--sni-hostname SNI_HOSTNAME
SNI hostname sent during the TLS handshake and used
for hostname verification. Useful when --url points at
an IP address or a load balancer that needs an
explicit SNI. Default uses the hostname from --url.
--source {file,scan,url}
Where the certificates are fetched from. `url` fetches
one from a TLS endpoint (requires --url). `file` reads
one or many from local files (requires --filename,
supports glob patterns). `scan` discovers the hosts of
a subnet (via --host, --network, --interface or the
default interface) and probes each one on --ports.
`p12` and `jks` are reserved for future expansion.
Default: scan
--timeout TIMEOUT Network timeout in seconds. Default: 8 (seconds)
--url URL URL of the TLS endpoint to inspect. Required when
--source=url. Example: `https://www.example.com/`
--verbose Makes this plugin verbose during the operation. Useful
for debugging and seeing what is going on under the
hood. Default: False
-w, --warning WARN WARN threshold for the time remaining until the
certificate expires. Accepts a Nagios range in days
(`14:`), a percentage of the total validity period
(`25%`, WARN when less than 25% of the lifetime is
left), or a duration with a unit (`14d`, `12h`, `2W`,
`1M`; WARN when less time than that is left).
Examples: `14:` `25%` `14d`. Default: 14:
Usage Examples¶
./cert --source=url --url=https://www.example.com/
Output (default, OK):
www.example.com, 61d left, chain verified|'cert_days_left'=61d;14:;5: 'tls_handshake_time'=0.07s;;;0
Alert relative to the certificate’s lifetime instead of a fixed number of days. Here WARN when less than 25% of the validity period is left, CRIT below 10% (adapts to short-lived 90-day certs and multi-year certs alike):
./cert --source=url --url=https://www.example.com/ --warning=25% --critical=10%
Alert on a fixed duration left, for example WARN below two weeks and CRIT below three days:
./cert --source=url --url=https://www.example.com/ --warning=2W --critical=3d
Self-signed certificate, default --severity=warn:
./cert --source=url --url=https://internal.example.com/
Output:
internal.example.com, 725d left, chain unverified (self-signed certificate) [WARNING]|'cert_days_left'=725d;14:;5: 'tls_handshake_time'=0.5s;;;0
Skip chain verification entirely, but still inspect the cert:
./cert --source=url --url=https://api.example.com/ --insecure
Pin a custom CA bundle for chain verification:
./cert --source=url --url=https://api.example.com/ --ca-file=/etc/pki/ca-trust/source/anchors/internal.pem
Attach an mTLS (mutual TLS; client certificate authentication):
./cert --source=url --url=https://api.example.com/ --client-cert=/etc/icinga2/client.pem --client-key=/etc/icinga2/client.key
Override the SNI hostname, for example when --url points at an IP:
./cert --source=url --url=https://10.0.0.5/ --sni-hostname=api.example.com
Inspect a local certificate file:
./cert --source=file --filename=/etc/letsencrypt/live/example.com/cert.pem
Batch-inspect every certificate under a directory via glob, including recursive descent. Note the single quotes - they keep the shell from expanding the wildcard before the plugin sees it:
./cert --source=file --filename='/etc/ssl/certs/*.pem' --warning=14: --critical=5:
./cert --source=file --filename='/etc/pki/**/*.crt'
./cert --source=file --filename='/etc/letsencrypt/live/**/cert.pem'
Output (glob matches multiple files, one expired):
3 certificates in /etc/ssl/certs/*.pem checked, worst expired 42 days ago (www2.example.com) [CRITICAL]
File ! Subject CN ! Status ! State
----------------+------------------+---------------------+-----------
/e/s/c/web1.pem ! www1.example.com ! 90d left ! [OK]
/e/s/c/web2.pem ! www2.example.com ! expired 42 days ago ! [CRITICAL]
/e/s/c/web3.pem ! www3.example.com ! 100d left ! [OK]
Inspect a non-HTTPS TLS service - point --url at the service’s TLS port. Each line is an independent check; --source=url inspects exactly one endpoint per run:
./cert --source=url --url=https://mail.example.com:993/ # IMAPS
./cert --source=url --url=https://ldap.example.com:636/ # LDAPS
./cert --source=url --url=https://smtp.example.com:465/ # SMTPS
Output (single endpoint, same one-line form as the first example):
mail.example.com, 61d left, chain verified|'cert_days_left'=61d;14:;5: 'tls_handshake_time'=0.07s;;;0
Full field/value table per certificate with --lengthy:
./cert --source=url --url=https://www.example.com/ --lengthy
Output:
www.example.com, 61d left, chain verified
Field ! Value
--------------------+----------------------------------------------------------------------------------------------------
Subject CN ! www.example.com
Issuer CN ! R13
Serial ! 5700EBF15B911BC3D902A0BFE488552C112
Signature Algorithm ! sha256WithRSAEncryption
Public Key ! RSA 4096 (e=65537)
SANs ! www.example.com
Not Before ! 2026-04-11T22:19:04Z
Not After ! 2026-07-10T22:19:03Z
SHA-256 Fingerprint ! BF:DF:BB:23:93:A9:31:CD:8B:B9:A9:61:18:6D:0A:07:2C:4E:1F:9A:55:D0:7B:38:E2:6C:A4:90:11:F3:5D:88
OCSP Must-Staple ! no
TLS Version ! TLSv1.3
Chain ! verified
Scan the default interface’s subnet on the default set of common TLS ports. This is also what the plugin does without any parameter:
./cert
./cert --source=scan
Scan an explicit network on a custom set of ports, excluding the gateway:
./cert --source=scan --network=192.0.2.0/24 --ports=443 --ports=8443 --ports=9443 --exclude=192.0.2.1
Scan a handful of named hosts (a hostname target sends SNI, so virtual hosts present the right certificate):
./cert --source=scan --host=mail.example.com --host=ldap.example.com --ports=993 --ports=636
Scan a subnet and trust your internal CA, so internally-issued certificates count as verified instead of raising a trust warning (self-signed certs are tolerated either way):
./cert --source=scan --network=192.0.2.0/24 --ca-file=/etc/pki/ca-trust/source/anchors/internal.pem
Output (scan, one host answered on two ports, one certificate close to expiry):
1/254 hosts responded, 2 certificates, worst 9d left (b.example.com) [WARNING]
Target ! Subject CN ! Status ! State
------------------+---------------+----------+----------
192.0.2.10:443 ! a.example.com ! 59d left ! [OK]
192.0.2.10:8443 ! b.example.com ! 9d left ! [WARNING]
States¶
OK if the certificate is within
--warningand--criticalthresholds, the chain verifies and the hostname matches. A--source=scanrun where no host answers is also OK.WARN if days remaining hits
--warning(default14:), or chain/hostname verification fails and--severityiswarn(default).CRIT if days remaining hits
--critical(default5:), the certificate is expired, or chain/hostname verification fails and--severityiscrit.UNKNOWN on connection errors, TLS handshake failures (
--source=url), missing--url, missing--filename, no host left to scan, invalid--ports, missingcryptography(orpsutilfor scan auto-discovery) Python module, or invalid command-line arguments.--always-oksuppresses all alerts and always returns OK.--insecurereports the chain as „verification skipped“ and never raises a chain-related state.With
--source=scan, the chain/trust check runs (without hostname verification): a valid self-signed certificate is tolerated, other trust failures raise the state via--severity. Expiry is always evaluated, and the worst state across all reachable certificates wins.
Perfdata / Metrics¶
Name |
Type |
Description |
|---|---|---|
cert_days_left |
Days |
Days remaining until the certificate’s |
certs_found |
Number |
|
hosts_responded |
Number |
|
hosts_total |
Number |
|
tls_handshake_time |
Seconds |
|
Troubleshooting¶
cryptography module not installed¶
Python module "cryptography" is not installed.
The plugin needs the cryptography library to parse certificates. Install it with dnf install python3-cryptography (RHEL) or pip install cryptography.
TCP connection fails¶
Cannot connect to host:port: ...
The TCP connection never established. Check DNS resolution, firewall rules, the port number in --url, and --timeout.
TLS handshake fails¶
TLS handshake failed for host:port: ...
The server rejected the TLS handshake, usually a TLS version mismatch, an unsupported cipher, missing SNI, or a required client certificate. Pass --sni-hostname when the server needs SNI, --client-cert / --client-key for client authentication, or verify the server’s supported TLS versions and ciphers.
Certificate chain does not verify¶
chain unverified (...)
The chain did not verify against the system trust store. The text in parentheses is the OpenSSL verification message; common values are self-signed certificate, unable to get local issuer certificate, certificate has expired, and Hostname mismatch, certificate is not valid for .... Pass --ca-file for internal CAs, --sni-hostname for hostname mismatches caused by SNI, or --insecure to bypass the check entirely.
Glob matches no files¶
No files match "<pattern>"
The glob did not expand to any path, either because the path is wrong or because the shell expanded the glob before the plugin saw it. Quote the glob pattern: --filename='/etc/ssl/**/*.pem' instead of --filename=/etc/ssl/**/*.pem.
Glob matches files but no certificates¶
No parseable certificates among N file(s) matching "<pattern>"
The glob matched files, but none of them looked like a certificate (no PEM BEGIN CERTIFICATE marker and no DER signature). Tighten the pattern (for example *.crt instead of *) or point --filename directly at a known certificate file.
Certificate file cannot be parsed¶
Cannot parse certificate from /path/to/file: ...
The file looks like a certificate (PEM marker present or DER prefix detected) but the content is corrupt or in an unsupported encoding. Inspect the file with openssl x509 -in /path/to/file -noout -text to see the underlying error.
Certificate file cannot be read¶
Cannot read /path/to/file: ...
The plugin could not read the file, typically a permissions problem: certificate files are sometimes mode 0600 and owned by the service account, so the monitoring user has no read access. Grant read permission, or copy the public certificate to a path the monitoring user can read.
Credits, License¶
Authors: Linuxfabrik GmbH, Zurich
License: The Unlicense, see LICENSE file.