SSL certificate expiry checker
Open a TLS connection to a host, read the peer certificate and return days remaining, issuer and subject without any extra packages.
LaravelUtilitiesssltlsmonitoringopenssl
Code
<?php
namespace App\Monitoring;
use RuntimeException;
final class SslExpiryChecker
{
/**
* @return array{host: string, issuer: string, subject: string, expires_at: \DateTimeImmutable, days_left: int}
*/
public function check(string $host, int $port = 443, int $timeout = 8): array
{
$host = strtolower(trim($host));
if (! preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/', $host)) {
throw new RuntimeException('Invalid host name.');
}
$context = stream_context_create(['ssl' => [
'capture_peer_cert' => true,
'SNI_enabled' => true,
'peer_name' => $host,
// We only read the dates, so an already-expired or self-signed cert must still connect.
'verify_peer' => false,
'verify_peer_name' => false,
]]);
$socket = @stream_socket_client(
"ssl://{$host}:{$port}", $errno, $error, $timeout, STREAM_CLIENT_CONNECT, $context
);
if ($socket === false) {
throw new RuntimeException("TLS connection failed: {$error} ({$errno})");
}
$params = stream_context_get_params($socket);
fclose($socket);
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate'] ?? '');
if (! $cert) {
throw new RuntimeException('Could not read the certificate.');
}
$expires = (new \DateTimeImmutable)->setTimestamp($cert['validTo_time_t']);
$seconds = $cert['validTo_time_t'] - time();
return [
'host' => $host,
'issuer' => $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? 'unknown',
'subject' => $cert['subject']['CN'] ?? $host,
'expires_at' => $expires,
// floor() so a cert that expired 3 hours ago reports -1, not 0.
'days_left' => (int) floor($seconds / 86400),
];
}
}
How it works
What it solves
Expired certificates are one of the most avoidable outages. Knowing how many days remain only needs one TLS handshake, so you do not need a third-party API or a package.
How it works
The class opens ssl://host:443 with a stream context that asks PHP to keep the peer certificate. SNI is enabled and peer_name is set, so servers hosting many domains hand back the right certificate. After the handshake, stream_context_get_params() returns the certificate resource and openssl_x509_parse() turns it into an array with validTo_time_t, issuer and subject.
Peer verification is deliberately off: the goal is to read dates, and a monitor should still report on a certificate that is already expired, self-signed or issued for a different name. The result is a small array with an expiry date and a days_left integer computed with floor(), so negative values clearly mean "already expired".
Adapting it
- Run it daily from the scheduler and alert at 30, 14 and 7 days left, remembering which threshold you already notified.
- Pass a different port to check mail or API servers that use implicit TLS.
- Compare
subjectwith the expected name if you also want to catch mismatched certificates.
Pitfalls
- Validate the host string; never let user input contain slashes or schemes, otherwise the monitor can be pointed at internal services.
- Private networks: block internal address ranges if tenants can add hosts.
- STARTTLS ports such as 587 need a different handshake.
How to use it
- Make sure the PHP
opensslextension is enabled (it is by default). - Put the class in
app/Monitoring/SslExpiryChecker.php. - Call it:
$info = app(SslExpiryChecker::class)->check('example.com'); - Compare
$info['days_left']with your thresholds and notify when it drops below one. - Wrap the call in try/catch: a refused connection throws a
RuntimeException.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.