A fast C utility that checks X.509 certificates for expiration and alerts you with the location and status of each cert. A Rust port is available in cck-rs/.
Caddy and Traefik handle cert lifecycle β they auto-provision via Let's Encrypt (ACME), auto-renew before expiry, and manage TLS termination. If everything is working, you never think about certs. That's exactly the failure mode cck is built around.
cck covers what reverse proxies don't:
- When auto-renewal silently fails β ACME rate limits, DNS challenge failures, a firewall blocking port 80, an expired API token for your DNS provider. The renewal failed; the proxy logged it; nobody looked at the logs.
- Certs the proxy didn't issue β internal CA certs, client certs, mTLS leaf certs, database TLS certs, anything not behind your reverse proxy.
- Infrastructure you don't own β upstream APIs, third-party vendors, partners. Their cert expires, your integration breaks, it's your 3am page.
- Legacy systems β the internal service that predates your proxy, has a manually issued cert, and nobody remembers where it came from.
- Non-HTTP TLS β Postgres, Redis, LDAP, gRPC, MQTT. Traefik and Caddy are not in that path.
- Compliance auditing β proving to a compliance team that cert health is actively monitored.
Caddy / Traefik β prevent expiry (lifecycle management)
cck β verify it worked (observability)
They're complementary. Caddy and Traefik are the thing that should make cck never fire. cck is the thing that tells you when they didn't.
- β File check β inspect PEM files, including chains with multiple certs
- π Live check β connect to a host and check its TLS certificate in real-time
- π΄ Expired β prints location and how many days ago it expired
- π‘ Expiring soon β warns if expiration is within a configurable threshold
- π’ OK β confirms the cert is valid with its expiry date
- Handles IPv6, custom ports, and SNI
- Connect timeout (
-t) prevents hung pipelines on slow or unreachable hosts - Integrates with Nagios/Icinga and Datadog β see
nagios/anddatadog/
- C11 compiler (clang or gcc)
- OpenSSL 1.1+ or 3.x
brew install opensslapt install libssl-dev # Debian/Ubuntu
dnf install openssl-devel # Fedora/RHELmakeOptional debug build:
make DEBUG=1Install to /usr/local/bin:
make installmake test # run both unit tests and smoke tests
make test-unit # C unit tests only
make test-smoke # shell smoke tests onlyDrive the internal C functions (evaluate_cert, check_file) directly using
certs constructed in memory via the OpenSSL API β no shell-outs, no temp dirs
needed by the test framework itself. Covers:
| Suite | Cases |
|---|---|
evaluate_cert β valid |
10-year cert β CERT_OK |
evaluate_cert β expiring soon |
cert within warn window β CERT_WARN |
evaluate_cert β boundary |
exact threshold / threshold+1 / warn=0 |
evaluate_cert β expired |
cert expired 1 year ago β CERT_EXPIRED |
evaluate_cert β verbose |
expired cert in -v mode doesn't crash |
evaluate_cert β quiet |
-q still returns correct status |
check_file β valid PEM |
single-cert file β CERT_OK |
check_file β expired PEM |
single-cert file β CERT_EXPIRED |
check_file β chain (mixed) |
valid + expired in one file β CERT_EXPIRED |
check_file β chain (all OK) |
two valid certs β CERT_OK |
check_file β missing file |
β CERT_ERROR |
check_file β non-PEM file |
β CERT_ERROR |
End-to-end tests that invoke the compiled cck binary. Certs are generated
with openssl req at the start of each run and cleaned up on exit. Covers:
- Exit codes β valid/expired/warn/missing/non-PEM/mixed/no-args/
--help - Output content β OK/EXPIRED/WARN labels, date format, days count, file path in output
- Quiet mode (
-q) β no stdout for valid certs, still prints on expired --no-colorβ no ANSI escape codes in output- PEM chains β multi-cert files: worst status wins, all certs reported
- Verbose (
-v) β expired cert shows valid-from date - Network β live TLS check against
example.com(auto-skipped when offline)
cck [OPTIONS] <file.pem> [file2.pem ...] check certificate files
cck [OPTIONS] -H <host[:port]> [...] check live TLS certs
Options:
-H <host[:port]> connect to host and check its TLS certificate
(port defaults to 443)
-w <days> warn if expiring within N days (default: 30)
-t <secs> TLS connect timeout in seconds (default: 10)
-q quiet mode β only show problems
-v verbose β show extra cert details
--no-color disable ANSI colour output
--help show this help
Exit codes:
0 all certificates valid
1 one or more certificates expired or expiring soon
2 error (file not found, parse failure, connection error)
# Check a local certificate file
cck /etc/ssl/certs/my.pem
# Check multiple files
cck /etc/nginx/ssl/*.pem
# Check a live TLS certificate
cck -H example.com
# Check a non-standard port
cck -H example.com:8443
# Warn if cert expires within 14 days
cck -H example.com -w 14
# Quiet mode β only print problems (useful in cron jobs)
cck -q /etc/ssl/certs/*.pem
# Verbose output
cck -v -H example.com
# Use in a script
if ! cck -q /path/to/cert.pem; then
echo "Certificate problem detected!" | mail -s "CERT ALERT" admin@example.com
fiOK /etc/ssl/my.pem
expires 2026-11-15 (in 172 days)
WARN /etc/ssl/old.pem
expires 2025-06-10 (in 14 days)
EXPIRED /etc/ssl/ancient.pem
expired 2024-01-01 (365 days ago)
Check certs nightly and alert on problems:
0 6 * * * /usr/local/bin/cck -q /etc/ssl/certs/*.pem || mail -s "CERT EXPIRED" admin@example.comNagios / Icinga (nagios/)
A compiled C plugin that follows the Nagios plugin interface exactly:
| Exit | State | Meaning |
|---|---|---|
| 0 | OK | Valid, not expiring soon |
| 1 | WARNING | Expires within -w days |
| 2 | CRITICAL | Expires within -c days, or already expired |
| 3 | UNKNOWN | Connection / parse error |
Output includes a single status line and days_remaining performance data for PNP4Nagios / Graphite:
SSL CERT OK - example.com:443 expires 2026-07-01 (in 35 days) | days_remaining=35;30;14;0;
SSL CERT CRITICAL - example.com:443 certificate EXPIRED on 2025-01-01 (147 days ago) | days_remaining=-147;30;14;0;
cd nagios && make && make install # installs to /usr/local/nagios/libexec
make test # 27 smoke testsSee nagios/README.md for command/service object definitions and full usage.
Datadog (datadog/)
A Python Datadog Agent check that reports:
| What | Detail |
|---|---|
cck.ssl.days_remaining |
Gauge β days until expiry (negative when expired) |
cck.ssl.cert |
Service check β OK / WARNING / CRITICAL / UNKNOWN |
Supports host:port live checks and local PEM files (including chains). Warning and critical thresholds are configurable globally in init_config or overridden per instance β useful when mixing long-lived and short-lived (e.g. Let's Encrypt) certs.
# Install
sudo cp datadog/checks.d/cck_ssl.py /etc/datadog-agent/checks.d/
sudo cp -r datadog/conf.d/cck_ssl.d /etc/datadog-agent/conf.d/
# edit /etc/datadog-agent/conf.d/cck_ssl.d/conf.yaml, then:
sudo systemctl restart datadog-agent
# Test (no Agent required)
python3 -m pytest datadog/tests/test_cck_ssl.py -v # 27 unit testsSee datadog/README.md for full config reference and a Terraform monitor example.
Rust Port (cck-rs/)
A full port of the utility to Rust, motivated by the security review findings on the C version (see below). The Rust implementation is feature-identical β same flags, same output format, same exit codes β but several of the C vulnerabilities cannot occur in idiomatic Rust:
| C finding | Why it can't happen in Rust |
|---|---|
BIO_new_ssl() return unchecked β NULL deref |
Option<T> / Result<T, E> make all fallible returns explicit; the compiler rejects unhandled cases |
ssl pointer used before NULL check |
Same β pointer-returning FFI calls return Option; you pattern-match before use |
atoi silently accepts garbage |
str::parse::<i64>() returns Result; invalid input is a compile-time-enforced error path |
alarm()/SIGALRM with async-signal-safety constraints |
TcpStream::connect_timeout + set_read_timeout handle timeouts at the OS level β no signal handler needed |
The C version has been hardened with explicit fixes for all of the above. The Rust port starts from a position where that class of mistake is structurally prevented.
cd cck-rs
cargo build --release # binary at target/release/cck
cargo run --release -- --helpRequires Rust 1.70+ and system OpenSSL (same version used by the C build).
A security review of cck.c was performed and the following issues were found and remediated:
| Severity | Finding | Resolution |
|---|---|---|
| π΄ High | BIO_new_ssl() return value not checked β NULL dereference crash on OOM |
Added NULL guard before BIO_get_ssl, SSL_set_tlsext_host_name, and BIO_push |
| π΄ High | ssl pointer not validated after BIO_get_ssl() before SNI call |
Explicit NULL check with early return added |
| π Med | No connect/handshake timeout β cck hangs forever on a slow or hostile peer | Added SIGALRM handler + alarm(g_timeout) wrapping both BIO_do_connect and BIO_do_handshake; exposed as -t <secs> (default 10) |
| π Med | ERR_print_errors_fp(stderr) dumped OpenSSL internals unconditionally on error |
Gated behind -v; calls ERR_clear_error() otherwise |
| π Med | atoi used on -w input β silently accepts garbage, no overflow protection |
Replaced with strtol + bounds check (0β36500); invalid input is rejected with a message |
| π‘ Low | SSL_get_peer_certificate deprecated since OpenSSL 3.0 |
Replaced with SSL_get1_peer_certificate |
| π‘ Low | Redundant second BIO_get_ssl call before cert retrieval |
Removed; ssl was already valid from the first call |
| π‘ Low | port_override variable was always NULL β dead code |
Removed |
| π‘ Low | SSL_VERIFY_NONE had no explanatory comment |
Documented: intentional, cck must see certs that may be self-signed or expired, no data is sent over the connection |
| βΉοΈ Info | Flags placed after the first -H argument are silently ignored by the getopt pass |
Documented in source with a KNOWN LIMITATION comment; put all flags before targets |
SSL_VERIFY_NONE β cck deliberately disables chain verification so it can inspect any cert a server presents, including self-signed certs and certs from private CAs not in the system trust store. The connection is read-only and immediately discarded after the cert is retrieved; no sensitive data is transmitted. If you add any feature that sends data over the connection, this setting must be revisited.
Signal safety β the SIGALRM handler uses only write(2) and _exit(2), both of which are listed as async-signal-safe in POSIX. printf/fprintf are not safe inside signal handlers and are not used there.
