SSL certificate expiry is worth checking before it becomes an outage. This guide covers every practical method for checking SSL certificate expiry — from a quick online tool to scriptable approaches you can integrate into CI pipelines.
If you just want a quick answer without touching the command line, the free SSL Certificate Checker tool checks any domain and shows you:
Enter a domain name, get the result instantly — no account, no installation, no command line.
The openssl command-line tool is available on virtually every Unix-like system and is the most widely used method for checking certificate details from the terminal.
echo | openssl s_client -connect yourdomain.com:443 \
-servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -dates
Output:
notBefore=Sep 10 12:00:00 2026 GMT
notAfter=Dec 9 11:59:59 2026 GMT
The -servername flag is required for SNI (Server Name Indication) — without it, you may get the wrong certificate on hosts serving multiple domains.
EXPIRY=$(echo | openssl s_client -connect yourdomain.com:443 \
-servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
# Linux (GNU date):
echo $(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 )) days remaining
# macOS (BSD date):
echo $(( ($(date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s) - $(date +%s)) / 86400 )) days remaining
echo | openssl s_client -connect yourdomain.com:443 \
-servername yourdomain.com -showcerts 2>/dev/null \
| openssl x509 -noout -text \
| grep -E "(Issuer|Subject|Not Before|Not After)"
# SMTP with STARTTLS (port 587)
echo | openssl s_client -connect mail.yourdomain.com:587 \
-starttls smtp 2>/dev/null | openssl x509 -noout -dates
# LDAPS (port 636)
echo | openssl s_client -connect ldap.yourdomain.com:636 2>/dev/null \
| openssl x509 -noout -dates
openssl s_client hangs waiting for stdin if you don't pipe echo into it. Always prefix with echo | or pipe /dev/null in scripts.
curl can check certificate validity without extracting dates explicitly. It's useful when you want a quick pass/fail check rather than the exact expiry date:
# Check if the cert is currently valid (exits non-zero if cert error):
curl -s -o /dev/null --max-time 10 https://yourdomain.com
echo $? # 0 = valid, non-zero = error
For more detail, use --verbose:
curl -v --max-time 10 https://yourdomain.com 2>&1 | grep -E "expire|SSL|TLS|cert"
curl's --cacert flag lets you test against a specific CA bundle, which is useful when validating certificates on internal networks with custom certificate authorities:
curl --cacert /path/to/custom-ca.pem https://internal.yourdomain.com
curl doesn't give you the expiry date in a machine-readable format. For scripting, use openssl. For a quick "is this cert valid right now" check, curl's exit code is the fastest approach.
Python's ssl module can retrieve certificate details programmatically. This is the right approach when you need to integrate certificate checking into a larger Python script or application:
import ssl
import socket
from datetime import datetime
def check_ssl_expiry(hostname, port=443):
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
expiry_str = cert['notAfter']
expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
days_remaining = (expiry_date - datetime.utcnow()).days
return {
'hostname': hostname,
'expiry_date': expiry_date.strftime('%Y-%m-%d'),
'days_remaining': days_remaining,
'issuer': dict(x[0] for x in cert['issuer']),
'subject': dict(x[0] for x in cert['subject']),
}
# Usage:
result = check_ssl_expiry('yourdomain.com')
print(f"{result['hostname']}: {result['days_remaining']} days remaining (expires {result['expiry_date']})")
# Check multiple domains:
domains = ['yourdomain.com', 'api.yourdomain.com', 'anotherdomain.com']
for domain in domains:
try:
info = check_ssl_expiry(domain)
status = '⚠️' if info['days_remaining'] < 30 else '✓'
print(f"{status} {domain}: {info['days_remaining']} days")
except Exception as e:
print(f"✗ {domain}: {e}")
For more advanced use cases — including checking the full certificate chain, validating against a custom CA, or extracting Subject Alternative Names — use the cryptography library instead of the stdlib ssl module:
pip install cryptography
import ssl
import socket
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from datetime import datetime, timezone
def get_cert_details(hostname, port=443):
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
der_cert = ssock.getpeercert(binary_form=True)
cert = x509.load_der_x509_certificate(der_cert, default_backend())
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
alt_names = [name.value for name in san.value]
now = datetime.now(timezone.utc)
days_remaining = (cert.not_valid_after_utc - now).days
return {
'subject': cert.subject.rfc4514_string(),
'issuer': cert.issuer.rfc4514_string(),
'not_before': cert.not_valid_before_utc.isoformat(),
'not_after': cert.not_valid_after_utc.isoformat(),
'days_remaining': days_remaining,
'san': alt_names,
'serial': hex(cert.serial_number),
}
Node.js's tls module provides certificate information via the getPeerCertificate() method:
const tls = require('tls');
function checkSSLExpiry(hostname, port = 443) {
return new Promise((resolve, reject) => {
const socket = tls.connect(
{ host: hostname, port, servername: hostname },
() => {
const cert = socket.getPeerCertificate();
socket.end();
if (!cert || !cert.valid_to) {
return reject(new Error('Could not retrieve certificate'));
}
const expiryDate = new Date(cert.valid_to);
const now = new Date();
const daysRemaining = Math.floor(
(expiryDate - now) / (1000 * 60 * 60 * 24)
);
resolve({
hostname,
subject: cert.subject,
issuer: cert.issuer,
validFrom: cert.valid_from,
validTo: cert.valid_to,
daysRemaining,
fingerprint: cert.fingerprint256,
});
}
);
socket.setTimeout(10000);
socket.on('timeout', () => reject(new Error('Connection timed out')));
socket.on('error', reject);
});
}
// Usage:
checkSSLExpiry('yourdomain.com')
.then(info => {
console.log(`${info.hostname}: ${info.daysRemaining} days remaining`);
if (info.daysRemaining < 30) {
console.warn('⚠️ Certificate expiring soon!');
}
})
.catch(err => console.error(`Error: ${err.message}`));
// Check multiple domains:
const domains = ['yourdomain.com', 'api.yourdomain.com'];
Promise.allSettled(domains.map(checkSSLExpiry)).then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
const { hostname, daysRemaining } = result.value;
console.log(`${daysRemaining >= 30 ? '✓' : '⚠️'} ${hostname}: ${daysRemaining} days`);
} else {
console.log(`✗ ${domains[i]}: ${result.reason.message}`);
}
});
});
Adding a certificate check to your deployment pipeline catches expiry issues at deploy time, surfacing them in your CI logs where engineers are already looking:
#!/bin/bash
# Add to your post-deploy CI step
DOMAIN="yourdomain.com"
WARN_DAYS=30
EXPIRY=$(echo | openssl s_client -connect "$DOMAIN":443 \
-servername "$DOMAIN" 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
echo "SSL cert for $DOMAIN: $DAYS days remaining (expires $EXPIRY)"
if [ "$DAYS" -lt "$WARN_DAYS" ]; then
echo "::warning::SSL certificate expires in $DAYS days — renewal required"
fi
if [ "$DAYS" -lt 0 ]; then
echo "::error::SSL certificate is EXPIRED"
exit 1
fi
GitHub Actions workflow step:
- name: Check SSL certificate
run: |
DOMAIN="${{ env.PRODUCTION_DOMAIN }}"
EXPIRY=$(echo | openssl s_client -connect "$DOMAIN":443 \
-servername "$DOMAIN" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
echo "Certificate expires in $DAYS days"
if [ "$DAYS" -lt 30 ]; then
echo "::warning::SSL cert for $DOMAIN expires in $DAYS days"
fi
This only runs on deploy, so it won't catch a cert that expires between deploys — it complements rather than replaces dedicated monitoring.
| Situation | Best method |
|---|---|
| Quick manual check, no terminal | SSL Certificate Checker online tool |
| Terminal, one-off check | openssl s_client |
| Quick pass/fail validation | curl -s -o /dev/null |
| Python application integration | ssl stdlib or cryptography library |
| Node.js application integration | tls.connect() + getPeerCertificate() |
| CI/CD pipeline check | openssl in a shell step |
| Shell script monitoring loop | openssl + cron job |
Every method above requires you to actively run a check. That's fine for a one-time inspection or integrating into a script you already run — but it's not a substitute for proactive monitoring.
The problem with manual checking:
The right workflow is: use manual checking (online tool or openssl) to audit your current state and troubleshoot specific certs, and use automated monitoring to get proactive expiry alerts across all your domains.
Check your certificates right now with the free SSL checker — then set up SSLWatch for automated 60/30/7-day expiry alerts so you never have to remember to check again.
Free SSL checker SSLWatch early access