Blob


1 #!/usr/bin/env perl
2 #
3 # Copyright (c) 2021 Omar Polo <op@omarpolo.com>
4 #
5 # Permission to use, copy, modify, and distribute this software for any
6 # purpose with or without fee is hereby granted, provided that the above
7 # copyright notice and this permission notice appear in all copies.
8 #
9 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 #
17 # You can read the documentation for this script using
18 #
19 # $ perldoc renew-certs
20 #
22 use v5.10;
23 use strict;
24 use warnings;
26 use Getopt::Std;
27 use Time::Piece;
29 my $auto = 0;
30 my $conf = '/etc/gmid.conf';
31 my $days = 365;
32 my $gmid = 'gmid';
33 my $restart = 0;
34 my $threshold = 24 * 60 * 60;
36 my %options = ();
37 getopts("ac:d:g:r", \%options);
39 foreach my $flag (keys %options) {
40 if ($flag eq 'a') {
41 $auto = 1;
42 } elsif ($flag eq 'c') {
43 $conf = $options{c};
44 } elsif ($flag eq 'd') {
45 $days = int($options{d}) or exit 1;
46 } elsif ($flag eq 'g') {
47 $gmid = $options{g};
48 } elsif ($flag eq 'r') {
49 $auto = 1;
50 $restart = 1;
51 } elsif ($flag eq 't') {
52 $threshold = int($options{t}) or exit 1;
53 }
54 }
56 my $now = localtime()->epoch + $threshold;
57 my $found_one = 0;
59 my $c = `$gmid -nn -c $conf @ARGV 2>/dev/null`;
60 die "$gmid failed to parse $conf" if $? != 0;
62 while ($c =~ /server \"(.*)\"/g) {
63 my $server = $1;
65 $c =~ /cert \"(.*)\"/gc;
66 my $cert = $1;
68 $c =~ /key \"(.*)\"/gc;
69 my $key = $1;
71 if (expired($cert)) {
72 $found_one = 1;
73 if ($auto) {
74 renew($server, $cert, $key);
75 } else {
76 say $server;
77 }
78 }
79 }
81 if ($found_one && $restart) {
82 my @cmd = ("pkill", "-HUP", $gmid);
83 system(@cmd);
84 }
86 exit !$found_one;
88 sub expired {
89 my ($cert) = @_;
91 my $exp = `openssl x509 -noout -enddate -in $cert`;
92 die 'failed to execute openssl' if $? != 0;
93 chomp $exp;
95 my $d = Time::Piece->strptime($exp, "notAfter=%b %e %T %Y %Z");
96 return $d->epoch < $now;
97 }
99 sub renew {
100 my ($hostname, $cert, $key) = @_;
101 my @cmd = (
102 "openssl", "req", "-x509",
103 "-newkey", "rsa:4096",
104 "-out", $cert,
105 "-keyout", $key,
106 "-days", $days,
107 "-nodes",
108 "-subj", "/CN=".$hostname,
109 );
111 system(@cmd) == 0
112 or die "system @cmd failed: $?";
115 __END__
117 =head1 NAME
119 B<renew-certs> - automatically renew gmid certificates
121 =head1 SYNOPSIS
123 B<renew-certs> [-ar] [-c I<conf>] [-d I<days>] [-g I<gmid>] [-t I<threshold>] [-- I<gmid flags...>]
125 =head1 DESCRIPTION
127 B<renew-certs> attempts to renew the certificates used by gmid if they
128 are close to the expiration date and can optionally restart the
129 server. It's meant to be used in a crontab(5) file.
131 B<renew-certs> needs at least B<gmid> 1.8.
133 The arguments are as follows:
135 =over
137 =item -a
139 Automatically generate a new set of certificates.
141 =item -c I<conf>
143 Path to the gmid configuration. By default is F</etc/gmid.conf>.
145 =item -d I<days>
147 Number of I<days> the newly generated certificates will be valid for;
148 365 by default.
150 =item -g I<gmid>
152 Path to the gmid(1) executable.
154 =item -r
156 Restart B<gmid> after re-generating the certificates by killing it
157 with SIGHUP. Implies -a.
159 =item -t I<threshold>
161 Tweak the expiring I<threshold>. Certificates whose I<notAfter> field
162 ends before I<threshold> seconds will be considered outdated. By
163 default is 86400, or 24 * 60 * 60, 24 hours.
165 =item I<gmid flags>
167 Additional flags to be passed to gmid(1).
169 =back
171 =head1 EXIT STATUS
173 The B<renew-certs> utility exits on 0 when at least one certificate is
174 about to expire and >0 otherwise, or if an error occurs.
176 =head1 EXAMPLES
178 Some examples of how to use B<renew-certs> in a crontab(5) file
179 follows:
181 # automatically renew and restart gmid
182 0 0 * * * renew-certs -r
184 # like the previous, but pass a custom flag to gmid
185 0 0 * * * renew-certs -r -- -Dcerts=/etc/ssl/
187 # automatically renew the certs but use a custom
188 # command (rcctl in this case) to restart the server
189 0 0 * * * renew-certs -a && rcctl restart gmid
191 # only check for expiration. `cmd' can read the names of the
192 # servers with an expiring certificate from stdin, one per
193 # line
194 0 0 * * * renew-certs | cmd
196 =head1 SEE ALSO
198 crontab(1) gmid(1) openssl(1) crontab(5)
200 =cut