π TLS/SSL is Everywhere#
TLS/SSL certificates used to be much harder to deploy than they are today. Generally you would have either created a self-signed certificate (not great) or purchased a certificate from a trusted organization (often expensive).
If you were purchasing, you might even have had to fax some information about yourself to the company that was selling the certificate. Maybe you had to wait for up to 48 hours to get your shiny, new certificate. There was enough friction involved in the process that TLS/SSL was not the norm.
This has all changed with the availability of free TLS/SSL certificates from Let’s Encrypt. There is now a much wider adoption of TLS/SSL encryption across the Internet. It is becoming less and less common to find web sites which don’t offer https. Wider adoption of TLS/SSL is a very good thing, but with this added layer of security there comes an added layer of complexity. Just like domain name registrations, TLS/SSL certificates need to be renewed regularly, otherwise they expire. This means you now have one more thing to monitor. 1
In this post we’ll look at a three different ways to automate getting TLS/SSL certificate expiration dates. 2 Before we get to that, is an easier way?
π There Is an Easier Way#
Getting an overview of all of your TLS/SSL certificates and their expiry dates can be cumbersome, so I created prettygoodping.com. It’s currently in a free beta.
Within a few clicks you’ll be able to set up a dashboard of your TLS/SSL certificates, giving you an easy overview of when they expire. You may also opt in to email notifications to remind you when you a certificate is in danger of expiring. If you sign up for the free beta now, you can monitor the expiry of up to 25 TLS/SSL certificates on your own personal dashboard.
π prettygoodping.com π
That’s the easy way, but where is the fun in that? Let’s look at doing it the hard way.
π¨ Building Our Own Solution#
While prettygoodping.com gives us a convenient way to do this, we can also roll our own solution. Let’s have a look at how to go about obtaining TLS/SSL cert expiration dates. 3
curl#
For our first example, let’s use curl
, which is already installed in a lot of places.
curl https://www.prettygoodping.com -vI --stderr - | \
grep " * expire date"
Our output is:
* expire date: Mar 9 22:27:01 2023 GMT
We can iterate on this to return just the date, by using a regex in Perl. We’ll
grab everything after :
and assume that’s the date.
curl https://www.prettygoodping.com -vI --stderr - | \
grep " * expire date" | \
perl -nE 'm/: (.*)/;say $1'
Our output is:
Mar 9 22:27:01 2023 GMT
That’s a helpful start. We could build something more complex that would do some date comparisons directly at the command line, but this seems like a good time to create a script.
How would this look in Go?
Golang#
In Go, we get the expiry date using only standard libraries – and with surprisingly few lines of code.
1// main checks a certificate expiration date
2package main
3
4import (
5 "crypto/tls"
6 "fmt"
7 "log"
8 "time"
9)
10
11func main() {
12 conn, err := tls.Dial("tcp", "www.prettygoodping.com:443", nil)
13 if err != nil {
14 log.Fatal("Could not find SSL certificate: " + err.Error())
15 }
16
17 expiry := conn.ConnectionState().PeerCertificates[0].NotAfter
18 fmt.Printf("Expiry: %v\n", expiry.Format(time.RFC850))
19}
Our output is:
Expiry: Thursday, 09-Mar-23 22:27:01 UTC
Done – next language, please!
π§ πͺ Perl#
The Net::SSL::ExpireDate module makes this trivial to do in Perl. A minimal example looks something like this:
1#!/usr/bin/env perl
2
3use 5.12.0; # use strict/use warnings/use feature qw( say )
4
5use Net::SSL::ExpireDate ();
6
7my $hostname = 'www.prettygoodping.com';
8
9my $ed = Net::SSL::ExpireDate->new(
10 https => $hostname,
11 timeout => 5,
12);
13
14my $date = $ed->expire_date;
15if (!$date) {
16 warn 'Could not find expiration date for ' . $hostname;
17 exit(1);
18}
19
20say sprintf( "%s expires at %s", $hostname, $date );
Our output is:
www.prettygoodping.com expires at 2023-03-09T22:27:01
The object which is returned by the expire_date()
method is a
DateTime object in UTC. If we want to do
some date math to see if the certificate is still valid, that’s easy enough to
do. (Note that DateTime->now
also returns an object in UTC). Changes begin at
line 22.
1#!/usr/bin/env perl
2
3use 5.12.0; # use strict/use warnings/use feature qw( say )
4
5use Net::SSL::ExpireDate ();
6
7my $hostname = 'www.prettygoodping.com';
8
9my $ed = Net::SSL::ExpireDate->new(
10 https => $hostname,
11 timeout => 5,
12);
13
14my $date = $ed->expire_date;
15if (!$date) {
16 warn 'Could not find expiration date for ' . $hostname;
17 exit(1);
18}
19
20say sprintf( "%s expires at %s", $hostname, $date );
21
22if ( $ed->expire_date < DateTime->now ) {
23 say 'Your certificate has already expired';
24}
25else {
26 for my $weeks ( 1 .. 12 ) {
27 if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
28 say sprintf(
29 'Your certificate expires in less than %i weeks',
30 $weeks
31 );
32 last;
33 }
34 }
35}
Our output is:
www.prettygoodping.com expires at 2023-03-09T22:27:01
Your certificate expires in less than 9 weeks
If we want to allow for warnings, our example looks something like:
1#!/usr/bin/env perl
2
3use 5.36.0;
4
5use Net::SSL::ExpireDate ();
6
7my $hostname = 'www.prettygoodping.com';
8
9my $ed = Net::SSL::ExpireDate->new(
10 https => $hostname,
11 timeout => 5,
12);
13
14my ( $date, $messages ) = ssl_expiry($hostname);
15
16if ( $messages->@* ) {
17 say 'The following issues were encountered:';
18 say $_ for $messages->@*;
19}
20
21exit 1 unless $date;
22
23say $hostname . ' expires at ' . $date;
24
25if ( $ed->expire_date < DateTime->now ) {
26 say 'Your certificate has already expired';
27}
28else {
29 for my $weeks ( 1 .. 12 ) {
30 if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
31 say sprintf(
32 'Your certificate expires in less than %i weeks',
33 $weeks
34 );
35 last;
36 }
37 }
38}
39
40sub ssl_expiry ( $hostname, $timeout = 5 ) {
41 my $warning;
42
43 # expire_date could generate non-fatal warnings. We will catch them here
44 # and return them later.
45 local $SIG{__WARN__} = sub { $warning = shift };
46 my $ed = Net::SSL::ExpireDate->new(
47 https => $hostname,
48 timeout => $timeout,
49 );
50
51 my $date = $ed->expire_date;
52 my @messages;
53
54 if ($warning) {
55 chomp $warning;
56 push @messages, $warning;
57 }
58
59 return ( $date, \@messages ) if $date;
60
61 push @messages,
62 'Could not find expiration date for a certificate at ' . $hostname;
63
64 return ( undef, \@messages );
65}
Let’s take a look at what changed:
diff --git a/02-minimal-with-date-math.pl b/03-with-error-handling.pl
old mode 100755
new mode 100644
index ac89ade..0c6b4b2
--- a/02-minimal-with-date-math.pl
+++ b/03-with-error-handling.pl
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-use 5.12.0; # use strict/use warnings/use feature qw( say )
+use 5.36.0;
use Net::SSL::ExpireDate ();
@@ -11,13 +11,16 @@ my $ed = Net::SSL::ExpireDate->new(
timeout => 5,
);
-my $date = $ed->expire_date;
-if (!$date) {
- warn 'Could not find expiration date for ' . $hostname;
- exit(1);
+my ( $date, $messages ) = ssl_expiry($hostname);
+
+if ( $messages->@* ) {
+ say 'The following issues were encountered:';
+ say $_ for $messages->@*;
}
-say sprintf( "%s expires at %s", $hostname, $date );
+exit 1 unless $date;
+
+say $hostname . ' expires at ' . $date;
if ( $ed->expire_date < DateTime->now ) {
say 'Your certificate has already expired';
@@ -33,3 +36,30 @@ else {
}
}
}
+
+sub ssl_expiry ( $hostname, $timeout = 5 ) {
+ my $warning;
+
+ # expire_date could generate non-fatal warnings. We will catch them here
+ # and return them later.
+ local $SIG{__WARN__} = sub { $warning = shift };
+ my $ed = Net::SSL::ExpireDate->new(
+ https => $hostname,
+ timeout => $timeout,
+ );
+
+ my $date = $ed->expire_date;
+ my @messages;
+
+ if ($warning) {
+ chomp $warning;
+ push @messages, $warning;
+ }
+
+ return ( $date, \@messages ) if $date;
+
+ push @messages,
+ 'Could not find expiration date for a certificate at ' . $hostname;
+
+ return ( undef, \@messages );
+}
This example is a little more robust. Note the use 5.36.0
near the top of the
script. Among other things, it automatically imports the say
function, turns
on strict
, warnings
, and
signatures
and postfix
dereferencing
I love this shorthand. I’ve been using it a lot lately.
We’re almost done. Let’s now add some command line flags so that we can avoid hardcoding the host.
1#!/usr/bin/env perl
2
3use 5.36.0;
4
5use English qw( *PROGRAM_NAME );
6use Getopt::Long::Descriptive qw( describe_options );
7use Net::SSL::ExpireDate ();
8
9my ( $opt, $usage ) = describe_options(
10 "$PROGRAM_NAME %o <some-arg>",
11 [ 'host=s', 'the server to connect to', { required => 1 } ],
12 [ 'timeout|t=i', 'timeout (in seconds)', { default => 5 } ],
13 [],
14 [ 'help', 'print usage message and exit', { shortcircuit => 1 } ],
15);
16
17print( $usage->text ), exit if $opt->help;
18my $hostname = $opt->host;
19
20my $ed = Net::SSL::ExpireDate->new(
21 https => $hostname,
22 timeout => $opt->timeout,
23);
24
25my ( $date, $messages ) = ssl_expiry($hostname);
26
27if ( $messages->@* ) {
28 say 'The following issues were encountered:';
29 say $_ for $messages->@*;
30}
31
32exit 1 unless $date;
33
34say $hostname . ' expires at ' . $date;
35
36if ( $ed->expire_date < DateTime->now ) {
37 say 'Your certificate has already expired';
38}
39else {
40 for my $weeks ( 1 .. 12 ) {
41 if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
42 say sprintf(
43 'Your certificate expires in less than %i weeks',
44 $weeks
45 );
46 last;
47 }
48 }
49}
50
51sub ssl_expiry ( $hostname, $timeout = 5 ) {
52 my $warning;
53
54 # expire_date could generate non-fatal warnings. We will catch them here
55 # and return them later.
56 local $SIG{__WARN__} = sub { $warning = shift };
57 my $ed = Net::SSL::ExpireDate->new(
58 https => $hostname,
59 timeout => $timeout,
60 );
61
62 my $date = $ed->expire_date;
63 my @messages;
64
65 if ($warning) {
66 chomp $warning;
67 push @messages, $warning;
68 }
69
70 return ( $date, \@messages ) if $date;
71
72 push @messages,
73 'Could not find expiration date for a certificate at ' . $hostname;
74
75 return ( undef, \@messages );
76}
Let’s take a look at what changed:
diff --git a/03-with-error-handling.pl b/06-lookup-script.pl
old mode 100644
new mode 100755
index 0c6b4b2..57d4e4d
--- a/03-with-error-handling.pl
+++ b/06-lookup-script.pl
@@ -2,13 +2,24 @@
use 5.36.0;
-use Net::SSL::ExpireDate ();
+use English qw( *PROGRAM_NAME );
+use Getopt::Long::Descriptive qw( describe_options );
+use Net::SSL::ExpireDate ();
-my $hostname = 'www.prettygoodping.com';
+my ( $opt, $usage ) = describe_options(
+ "$PROGRAM_NAME %o <some-arg>",
+ [ 'host=s', 'the server to connect to', { required => 1 } ],
+ [ 'timeout|t=i', 'timeout (in seconds)', { default => 5 } ],
+ [],
+ [ 'help', 'print usage message and exit', { shortcircuit => 1 } ],
+);
+
+print( $usage->text ), exit if $opt->help;
+my $hostname = $opt->host;
my $ed = Net::SSL::ExpireDate->new(
https => $hostname,
- timeout => 5,
+ timeout => $opt->timeout,
);
my ( $date, $messages ) = ssl_expiry($hostname);
The biggest change is the use of Getopt::Long::Descriptive. It’s not a lot of lines of code, but it gives us a few things.
- We can use command line args to specify host and timeout values
- Some rudimentary validation will happen in the argument parsing
- We get some help text if a command is incorrect or if
--help
is invoked
We can now invoke our script to look up an arbitrary host:
./06-lookup-script.pl --host www.prettygoodping.com
Let’s explore the other options as well. We can specify a timeout:
./06-lookup-script.pl --host www.prettygoodping.com --timeout 5
We can specify an arbitrary port:
./06-lookup-script.pl --host smtp.gmail.com:465
We can use an IP address rather than a host name:
./06-lookup-script.pl --host 142.250.180.14
With our error handling, we now get a helpful error message when something goes wrong. Here we have provided an invalid host:
./06-lookup-script.pl --host www.prettygoodping.comx
The following issues were encountered:
cannot create socket: Invalid argument at 06-lookup-script.pl line 62.
Could not find expiration date for a certificate at www.prettygoodping.comx
Since there was a problem, we get a non-zero exit code:
$ echo $?
1
If we want more detailed debugging information, we can use
Devel::Confess. Using it via the
PERL5OPT
environment variable is a nice trick, as it means you don’t need to
change the invocation of the script itself.
PERL5OPT="-MDevel::Confess" ./06-lookup-script.pl --host www.prettygoodping.comx
The following issues were encountered:
cannot create socket: Invalid argument at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 182.
Net::SSL::ExpireDate::_peer_certificate("www.prettygoodping.comx", 443, 5, undef) called at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 98
eval {...} called at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 98
Net::SSL::ExpireDate::expire_date(Net::SSL::ExpireDate=HASH(0x7fb4ce824fc8)) called at 06-lookup-script.pl line 62
main::ssl_expiry("www.prettygoodping.comx") called at 06-lookup-script.pl line 25
Could not find expiration date for a certificate at www.prettygoodping.comx
We also get helpful output when we call the script with no arguments or with --help
.
06-lookup-script.pl [-t] [long options...] <some-arg>
--host STR the server to connect to
--timeout INT (or -t) timeout (in seconds)
--help print usage message and exit
Note that the name of the script is injected into the help text via
$PROGRAM_NAME
, which is available when we import English
. For most of us
this is much clearer and easier to remember than using $0
. See
perlvar
for more info.
Just copy the script above, name it as you see fit and the name of your script will be used in the help text.
Have Fun With It#
By now you have enough information to be dangerous when it comes to finding and monitoring TLS/SSL certificate expiration dates. You can run the above script via a cron or even embed some of the logic into a pluggable monitoring system. If all of that seems to be too much, prettygoodping.com is available to you as an easy option as well.
The important thing is that you now have additional tools to make yourself aware of expiring certs well before there’s any danger of that happening. π
Attribution#
"File:Love padlocks on the Butchers' Bridge (Ljubljana).jpg" by Petar MiloΕ‘eviΔ is licensed under CC BY-SA 4.0 .
Of course, there are ways to automate TLS/SSL cert renewals, but that doesn’t mean they don’t need to be monitored. Your automation could fail and you might not realize until it’s too late. Having an extra safeguard in place is a good plan. ↩︎
I should note that if you’re using Let’s Encrypt, you can be notified by them via email of upcoming expirations. This is great, but relying on e-mail delivery alone is not very robust. This also doesn’t give you an on-demand overview of expiration dates. It’s a good thing to enable notification emails from Let’s Encrypt and you should do it, but we can do more. ↩︎
Thanks to https://nickjanetakis.com/blog/using-curl-to-check-an-ssl-certificate-expiration-date-and-details for getting me started on the right path. ↩︎