Certificate Authority Manager
-
Certificate Authority Manager
may add in the future? after checking the number of days prior to the expiration of the certificate. and screw for example a script found on the Internet.
–---
It turns out that to test enough to go through all ~ / .TinyCA / CAName / certs / *. Pem files. Script to perl, which checks the validity of the certificate. Script exits with code 2 if the certificate has expired, with code 1 if expire in two weeks and zero if the certificate is OK.run: check_certs /usr/local/OpenSSL/newcerts
check_certs
#!/bin/sh if [ ! -d "$1" ]; then echo "Unable to find directory: $1" exit 1 fi for sslcert in "$1"/*.pem do if [ -f "$sslcert" ]; then /usr/local/OpenSSL/check/check-certificate-expire "$sslcert" else echo "File $sslcert not found" fi done
check-expire
#!/usr/bin/perl # # Expiration date checker and handler. Supports x509 certificates, and # anything a wrapper script can be written for. Run perldoc(1) on this # script for additional documentation. # # The author disclaims all copyrights and releases this script into the # public domain. use strict; use warnings; use Config::General qw(ParseConfig); use Date::Parse qw(str2time); use File::Basename qw(basename); use File::Temp qw(tempfile); use Getopt::Std; END { # Report problems when writing to stdout (perldoc perlopentut) unless ( close(STDOUT) ) { warn "error: problem closing STDOUT: $!\n"; exit 74; } } my $basename = basename($0); my %program_param = ( name => $basename, argv => "@ARGV", pid => $$ ); # list different modes of operation (subroutine that obtains expiration # date and other metadata from an external command/code) my %actions = ( certificate => \&action_certificate, openssl_x509 => \&action_openssl_x509, ); my @actions = sort keys %actions; my %opts; getopts 'h?l:c:f:', \%opts; if ( exists $opts{l} ) { print "@actions\n"; do_exit(0); } if ( exists $opts{h} or exists $opts{'?'} or not @ARGV ) { print_help(); do_exit(64); } my %config = ParseConfig( -ConfigFile => $opts{f}, -LowerCaseNames => 1 ); if ( !defined $config{class} ) { remark( 'error', 'no classes defined in configuration', { file => $opts{f} } ); do_exit(78); } my $op_mode = shift || q{}; $op_mode =~ tr/-/_/; if ( !exists $actions{$op_mode} ) { remark( 'error', 'unknown mode', { allowed => join( q{,}, @actions ), mode => $op_mode } ); print_help(); do_exit(78); } # where in the prefs to read expire, window handlers from my $report_handler = exists $opts{c} ? $opts{c} : 'default'; my $report_ref = $config{class}->{$report_handler}; if ( !defined $report_ref ) { remark( 'error', 'class preferences not found', { class => $report_handler, file => $opts{f} } ); do_exit(78); } # lookup expiration date and other info via mode handler my ( %time_param, %extra_param ); { my ( $time_ref, $extra_ref ) = $actions{$op_mode}->(@ARGV); if ( !defined $time_ref or !defined $time_ref->{expire_epoch} ) { remark( 'error', 'no expire date found' ); do_exit(78); } @time_param{ keys %$time_ref } = values %$time_ref; @extra_param{ keys %$extra_ref } = values %$extra_ref; } $time_param{cur_epoch} = time; my %lookup = ( time => \%time_param, extra => \%extra_param, program => \%program_param ); # check whether expired, or if within a window if ( $time_param{expire_epoch} <= $time_param{cur_epoch} ) { # TODO fill out more parameters (humanized time? strftime, parameters # about the certificate, and etc.? - see what need for reporting) # seconds_ago => $cur_epoch - $expire_epoch, handle_condition( $report_ref->{expired}, \%lookup ); die "error: expired handler did not exit script\n"; } if ( exists $report_ref->{window} ) { my @windows; if ( ref $report_ref->{window} eq 'HASH' ) { my %tmp = %{ $report_ref->{window} }; delete $report_ref->{window}; $report_ref->{window}->[0] = \%tmp; } elsif ( ref $report_ref->{window} ne 'ARRAY' ) { remark( 'error', 'unexpected reference type for window', { ref => ref $report_ref->{window}, file => $opts{f} } ); do_exit(78); } # build Windows to look at (sanity checks, duration conversion) # TODO include window timevalue in time_param hash for my $window_ref ( @{ $report_ref->{window} } ) { if ( !exists $window_ref->{inside} ) { remark( 'error', 'skipping window without date setting' ); next; } $window_ref->{inside_sec} = duration2seconds( $window_ref->{inside} ); push @windows, $window_ref; } for my $window_ref ( sort { $a->{inside_sec} <=> $b->{inside_sec} } @windows ) { if ( $time_param{expire_epoch} <= ( $time_param{cur_epoch} + $window_ref->{inside_sec} ) ) { # TODO seconds_left => $expire_epoch - $cur_epoch, handle_condition( $window_ref, \%lookup ); die "error: window handler did not exit script\n"; } } } if ( exists $report_ref->{default} ) { # TODO seconds_left => $expire_epoch - $cur_epoch, handle_condition( $report_ref->{default}, \%lookup ); die "error: default handler did not exit script\n"; } else { # if no default, exit, assuming an implicit default exit do_exit(0); } # Mode to parse for expiration date using 'openssl x509' (certificate) sub action_openssl_x509 { my @x509_arguments = @_; my @command = ( qw{openssl x509 -noout -dates -subject -issuer}, @x509_arguments ); my $results = get_output(@command); if ( !defined $results ) { remark( 'error', 'no output returned', { command => "@command" } ); do_exit(70); } my ( %time_param, %cert_param ); for my $line (@$results) { if ( $line =~ m/^notAfter=(.+)/ ) { $time_param{expire_epoch} = date_openssl2epoch($1) if !exists $time_param{expire_epoch}; next; } if ( $line =~ m/^notBefore=(.+)/ ) { $time_param{start_epoch} = date_openssl2epoch($1) if !exists $time_param{start_epoch}; next; } if ( $line =~ m/^subject\s*=\s*(.+)/ ) { $cert_param{subject} = $1 if !exists $cert_param{subject}; next; } if ( $line =~ m/^issuer\s*=\s*(.+)/ ) { $cert_param{issuer} = $1 if !exists $cert_param{issuer}; next; } } if ( !exists $time_param{expire_epoch} ) { return; } return \%time_param, \%cert_param; } # runs a command that should return a certificate to stdout that # 'openssl x509' can then parse for information sub action_certificate { my @command = @_; my $results = get_output(@command); if ( !defined $results ) { remark( 'error', 'no output returned', { command => "@command" } ); do_exit(70); } my ( $tmp_fh, $filename ); eval { ( $tmp_fh, $filename ) = tempfile(); }; local $SIG{TERM} = sub { close $tmp_fh; unlink $filename }; if ( $@ or !defined $tmp_fh ) { remark( 'error', 'no temporary file created', ( $@ ? do { chomp $@; { errstr => $@ } } : {} ) ); do_exit(73); } for my $line (@$results) { print $tmp_fh $line; } close $tmp_fh; return $actions{openssl_x509}->( '-in', $filename ); } # parses handler prefs from under config, figures out what to do... sub handle_condition { my $handle_ref = shift; my $lookup_ref = shift; # handlers must exit, default to 0 if unset if ( !exists $handle_ref->{exit_value} ) { $handle_ref->{exit_value} = 0; } my %handlers = ( # runs a command, such as logger(1) exec => sub { my $cmd_str = shift; my $lookup_ref = shift; my @command = parse_tokens($cmd_str); for my $part (@command) { $part =~ s/ (?{$1}->{$2} || '' /egx; $part =~ s/(\\.)/qq("$1")/eeg; } my $status = system @command; if ( $status != 0 ) { remark( 'warning', 'command failed', { command => "@command", errno => $? } ); } return; }, # like exec, but takes first token as standard input to the program # (to support sending data to things like mail(1)) pipe => sub { my $cmd_str = shift; my $lookup_ref = shift; # standard input everything before first unbackwhacked | my ( $stdin, $cmd_tmp ) = split /\s*(? $stdin =~ s/ (?{$1}->{$2} || '' /egx; $stdin =~ s/(\\.)/qq("$1")/eeg; my @command; for my $part ( parse_tokens($cmd_tmp) ) { $part =~ s/ (?{$1}->{$2} || '' /egx; $part =~ s/(\\.)/qq("$1")/eeg; push @command, $part; } my $cmd_fh; open $cmd_fh, '|-' or exec @command or return; print $cmd_fh $stdin; close $cmd_fh; return; }, # print to standard output stdout => sub { my $output_str = shift; my $lookup_ref = shift; $output_str =~ s/ (?{$1}->{$2} || '' /egx; $output_str =~ s/(\\.)/qq("$1")/eeg; print "$output_str\n"; return; } ); for my $handle ( sort keys %handlers ) { if ( exists $handle_ref->{$handle} ) { if ( ref $handle_ref->{$handle} eq 'ARRAY' ) { for my $item ( @{ $handle_ref->{$handle} } ) { $handlers{$handle}->( $item, $lookup_ref ); } } else { $handlers{$handle}->( $handle_ref->{$handle}, $lookup_ref ); } } } do_exit( $handle_ref->{exit_value} ); } # converts a string into a list of tokens sub parse_tokens { my $string = shift; my @tokens; UBLE: { # non-quoted strings, backslashed quotes and whitespace allowed push( @tokens, $1 ), redo UBLE if $string =~ m/ \G ( [^"'\s]+ ) \s* /cgx; # double-quoted strings, backslashed quotes allowed push( @tokens, $1 ), redo UBLE if $string =~ m/ \G " ((?: \\.|[^\\"] )+) " \s* /cgx; push( @tokens, $1 ), redo UBLE if $string =~ m/ \G ' ((?: \\.|[^\\'] )+) ' \s* /cgx; last UBLE if $string =~ / \G $ /gcx; remark( 'error', 'unparseable token in string', { data => $string } ); do_exit(78); } return @tokens; } sub date_openssl2epoch { my $date = shift; my $time = str2time($date); return $time; } # takes command to run (and optional leading hashref with parameters), # returns filehandle (or undef on error) with STDOUT of program sub get_output { my $param = {}; if ( @_ and ref $_[0] eq 'HASH' ) { $param = { %$param, %{ $_[0] } }; shift @_; } my @command = @_; return unless @command; #remark( 'debug', 'command run', { command => "@command" } ); my $timeout = $param->{timeout} || 60; my @results; eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; my $output_fh; open $output_fh, '-|' or exec @command or die "exec error\n"; @results = <$output_fh>; close $output_fh; alarm 0; }; if ($@) { my $error_str = $@ eq "alarm\n" ? 'command timed out' : $@ eq "exec error\n" ? undef : 'unexpected command error'; if ( defined $error_str ) { chomp $@; remark( 'error', $error_str, { command => "@command", errno => $@ } ); } } return @results ? \@results : undef; } sub duration2seconds { my $tmpdur = shift; my $seconds; # how to convert short human durations into seconds my %factor = ( y => 31536000, w => 604800, d => 86400, h => 3600, m => 60, s => 1, ); # assume raw seconds for plain number if ( $tmpdur =~ m/^\d+$/ ) { $seconds = $tmpdur * 60; } elsif ( $tmpdur =~ m/^[ywdhms\d\s]+$/ ) { # match "2m 5s" style input and convert to seconds while ( $tmpdur =~ m/(\d+)\s*([ywdhms])/g ) { $seconds += $1 * $factor{$2}; } } else { remark( 'error', 'unknown characters in duration', { data => $tmpdur } ); do_exit(78); } unless ( defined $seconds and $seconds =~ m/^\d+$/ ) { remark( 'error', 'unable to parse duration', { data => $tmpdur } ); do_exit(78); } return $seconds; } # Wrapper for exit values, in case need to alter them under particular # monitoring systems. Script uses 100+ for various error conditions. sub do_exit { my $exit_value = shift; exit $exit_value; } sub remark { my $priority = shift; my $message = shift; my $attributes = shift; chomp $message; my $attr_str; if ($attributes) { $attr_str = join ', ', map { $attributes->{$_} ||= q{}; "$_=$attributes->{$_}" } sort keys %$attributes; } print STDERR "$priority: $message" . ( $attr_str ? ": $attr_str" : q{} ) . "\n"; return 1; } sub print_help { print <<"END_USAGE"; Usage: $basename -f prefs [options] mode [arguments] Expiration date handler. Options: -h/-? Display this message. -f pp Read preferences file from pp. -c cc Specify custom class to read from preferences file. -l List supported modes and exit. Run perldoc(1) on this script for additional documentation. END_USAGE return; } __END__ =head1 NAME check-expire - expiration date checker and handler =head1 SYNOPSIS Check the certificate on C<www.example.org:443>, and handle the expired or near-expired certificate via options in the C <prefs>configuration file: $ check-expire -f prefs certificate check-web www.example.org:443 Where the C <check-web>wrapper outputs the certificate via: #!/bin/sh echo GET / HTTP/1.0 | openssl s_client -connect "$1" 2>&1 Directly accessible certificate files can be checked via: $ check-expire -f prefs openssl-x509 -in /path/to/some.cert =head1 DESCRIPTION =head2 Overview Checks expiration dates on data like x509 certificates. Uses preferences to handle expired certificates, or configurable actions should the expiration date be inside a particular time window. Requires wrapper scripts to obtain the certificate or expiration date to parse. =head2 Normal Usage $ check-expire -f prefs mode [mode options] See L<"OPTIONS"> for details on the command line switches supported. =head1 OPTIONS This script currently supports the following command line switches. Arguments following the C <mode>will vary. =over 4 =item B<-h>, B<-?> Prints a brief usage note about the script. =item B<-f> I <prefs>Read handling preferences from the I <prefs>file. See L<"FILES"> for configuration details. =item B<-c> I <class>Specify a custom handling class, otherwise set to C<default>. The class is read from the preferences file, see L<"FILES"> for details. =item B<-l> List allowed modes to standard output and exit. =back =head1 FILES The preferences file specifies how to handle expiration dates. See below for an example. At minimum, a named C <class>block should be created with the name C<default>. This block should contain rules to handle different conditions: C <expired>will be called if the data is expired, followed by any C <window>blocks, ordered to process the shortest durations first. If none match, a C <default>hander is called. The first matching handler block will exit the script, with an C <exit_value>of zero, unless a different C <exit_value>is set. Blocks can include C <stdout>to print output, C <exec>to run named commands, C <pipe>to send input to a named command, and C <exit_value>to change the exit status of C<check-expire>. Aruguments to these options can template information about the certificate and other data (see the example below for the syntax, and the source for available parameters). C <exec>and C <pipe>must be commands to run, not shell statements. If shell code is needed, write a wrapper script, and execute that. C <window>handlers must include a single C <inside>statement followed by a duration in sections, or a shorthand duration such as C<7d>. The following outputs values read by SiteScope. For expired or near- expired data, errors are raised in SiteScope via the higher return codes. Data expiring inside a month generates an e-mail, but no SiteScope error. <class default=""># default handler used if nothing else matches <default>stdout "Return Code: 0"</default> # higher return code if expiring inside 7 days, e-mail warnings <window>inside 7d stdout "Return Code: 1" exit_value 1 pipe expiring soon: %{extra.subject} | /bin/mail -s "cert warning" root</window> # just warn via e-mail if expiring inside 30 days <window>inside 30d stdout "Return Code: 0" pipe expiring soon: %{extra.subject} | /bin/mail -s "cert warning" root</window> # handler for expired data (equal to or past expiration date) <expired>exec /usr/bin/logger expired certificate: subject=%{extra.subject} pipe <<end_pipe<br>Expired certificate: subject=%{extra.subject} expired=%{time.expire_epoch} command=%{program.name} %{program.argv} | /bin/mail -s "expired cert" root END_PIPE stdout "Return Code: 2" exit_value 2</end_pipe<br></expired></class> See L <config::general|config::general>for details on the configuration file format. =head1 BUGS =head2 Reporting Bugs Newer versions of this script may be available from: http://github.com/thrig/sial.org-scripts/tree/master If the bug is in the latest version, send a report to the author. Patches that fix problems or add new features are welcome. =head2 Known Issues No known issues. =head1 TODO Mode handler for key=value output from a program, to facilitate interface scripts to other arbitrary systems. More metadata in %lookup (with modifying %lookup too much once doing tests!), for humanized dates, time durations, and other details. Document usage under Nagios or other interfaces. Read things to check from preferences file and loop over, instead of needing a different command run for each thing to check? =head1 SEE ALSO perl(1), s_client(1), x509(1) =head1 AUTHOR Jeremy Mates =head1 COPYRIGHT The author disclaims all copyrights and releases this script into the public domain. =cut</config::general|config::general></inside></window></pipe></exec></check-expire></exit_value></pipe></exec></stdout></exit_value></exit_value></default></window></expired></default></class></default></class></prefs></prefs></mode></check-web></prefs></www.example.org:443>
check-expire.cfg
<class default=""><expired>exit_value 2</expired> <window>inside 2w exit_value 1</window></class>
check-certificate-expire
#!/bin/sh openssl_get_CN_from_file() { if [ ! -f "$1" ]; then echo "Unable to find file: $1" return 1 fi X509_CN=`openssl x509 -in "$1" -noout -subject | perl -pi -e 's/^.*\/?CN=([^\/]*).*$/\1/'` echo $X509_CN } openssl_get_SN_from_file() { if [ ! -f "$1" ]; then echo "Unable to find file: $1" return 1 fi X509_SN=`openssl x509 -in "$1" -noout -serial` echo $X509_SN } if [ ! -f "$1" ]; then echo "Unable to find file: $1" exit 1 fi /usr/local/OpenSSL/check/check-expire -f /usr/local/OpenSSL/check/check-expire.cfg openssl_x509 -in "$1" case $? in 1) X509_CN=`openssl_get_CN_from_file "$1"` X509_SN=`openssl_get_SN_from_file "$1"` if [ ! -z "$X509_CN" ]; then echo "$X509_CN, $X509_SN: certificate expires soon" else echo "$1: certificate expires soon" fi ;; 2) X509_CN=`openssl_get_CN_from_file "$1"` X509_SN=`openssl_get_SN_from_file "$1"` if [ ! -z "$X509_CN" ]; then echo "$X509_CN, $X509_SN: certificate expired!" else echo "$1: certificate expired!" fi ;; esac
Thank You