A cert expired on one of our staging services last month. Nobody noticed for two hours because our monitoring only checked HTTP 200s, and the TLS error happened before HTTP even got involved. The load balancer just refused connections silently.
Staging, so low impact. But it got me thinking about how I was checking certs across all our services. The honest answer: I mostly wasn't.
the script I used to have
I had a bash script that looked roughly like this:
#!/bin/bash
HOSTS="api.example.com staging.example.com admin.example.com"
for host in $HOSTS; do
expiry=$(echo | openssl s_client -connect "$host:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
echo "$host: $expiry"
done
It worked. Sort of. The output was a list of dates in openssl's format (Jun 15 12:00:00 2026 GMT), which is fine for a human glancing at it but annoying to parse if you want to do anything programmatic. And if a host was unreachable, the script would hang for the TCP timeout before moving on.
I also had no easy way to say "tell me which ones expire within 30 days." I'd just eyeball the dates.
what I switched to
I've been building a cert tool called sslx and one of the commands that turned out to be the most useful is expiry. It checks multiple hosts in one go:
$ sslx expiry api.example.com staging.example.com admin.example.com
Host Expires Days Status
────────────────────────────────────────────────────────────────
✓ api.example.com:443 2026-08-12 119 OK
✓ staging.example.com:443 2026-04-22 7 WARNING
✓ admin.example.com:443 2026-05-30 45 OK
The status column does the date math for me. OK means more than 30 days. WARNING is 30 days or less. CRITICAL is 7 days or less. EXPIRED is, well, expired.
The exit code is 1 if anything is expired or within 7 days. That's the part that makes it useful in automation.
putting it in a cron job
Here's what I actually run now:
# /etc/cron.d/cert-check (runs daily at 9am)
0 9 * * * gagan sslx expiry \
api.prod.example.com \
web.prod.example.com \
staging.example.com \
admin.example.com \
|| echo "cert expiring soon" | mail -s "cert warning" ops@example.com
Because the exit code is non-zero when something is expiring, the || branch triggers and sends an email. Simple. No parsing dates in bash, no jq gymnastics.
For something more structured, you can use JSON output:
sslx expiry api.example.com --json | jq '.[] | select(.days_remaining < 30)'
in CI/CD pipelines
I also added an expiry check to our deployment pipeline. We verify the target environment's certs aren't about to expire before deploying. After the staging thing I figured why not:
# .github/workflows/deploy.yml
- name: check target certs
run: |
sslx expiry ${{ vars.DEPLOY_HOST }} || {
echo "::error::TLS cert expiring soon on ${{ vars.DEPLOY_HOST }}"
exit 1
}
If the cert is expiring within a week, the deploy fails. We'd rather catch it there than find out at 2am.
checking local cert files too
The same tool handles local cert files, not just live hosts. If you have certs sitting on disk (maybe pulled from a vault, or generated for local dev), you can inspect them the same way:
$ sslx inspect cert.pem
╭─ Certificate 1 of 1 ────────────────────────────────────╮
│ Subject: CN=*.example.com │
│ Issuer: CN=Let's Encrypt Authority X3 │
│ │
│ Valid: 2026-01-15 → 2026-04-15 │
│ Expires: ██░░░░░░░░ 12 days remaining [!] │
│ │
│ Key: ECDSA P-256 (256 bit) │
│ SANs: *.example.com, example.com │
╰─────────────────────────────────────────────────────────╯
That progress bar is surprisingly useful. Red at a glance.
You can also pipe certs from other tools. This one comes up a lot with kubernetes:
kubectl get secret tls-cert -o jsonpath='{.data.tls\.crt}' \
| base64 -d \
| sslx inspect -
what I'm not doing here
I'm not replacing proper cert management. If you're using cert-manager or ACME renewals, those handle rotation automatically and you probably don't need to manually track expiry dates.
But I've found there's always a handful of certs that don't fit neatly into the automated renewal pipeline. Internal services with self-signed certs. Third party integrations where someone uploaded a cert manually six months ago. Staging environments that nobody quite owns.
Those are the ones that expire at the worst time. And those are the ones worth checking daily with a simple cron job.
install
cargo install sslx
# or
brew install glincker/tap/sslx
Single binary, no dependencies. Works on Linux, macOS, and Windows.
Top comments (0)