Отслеживаем количество дней до истечения срока сертификата TLS
Шаг 1. Установка дополнительных пакетов CentOS/RedHat yum install perl-Date-Calc perl-Crypt-OpenSSL-X509
Ubuntu/Debian apt-get install libdate-calc-perl libcrypt-openssl-x509-perl
Шаг 2. Добавляем скрипт проверки Добавим скрипт проверки сертификата, для этого создаем новый файл /usr/lib64/nagios/plugins/check_tls_certificate_expiration
со следующим содержимым:
use strict;use warnings;use Getopt::Long;use Date::Calc qw(Delta_Days Parse_Date Today) ;use Crypt::OpenSSL::X509;our $ARG_WARNING_DAYS = 14 ;our $ARG_CRITICAL_DAYS = 7 ;our $ARG_ADDRESS = '' ;our $ARG_HOSTNAME = '' ; our $ARG_PORT = 443 ;our $ARG_OPENSSL = '/usr/bin/openssl' ;our $ARG_STARTTLS = '' ;our $ARG_FILE = '' ;our $ARG_COMMON_NAME = '' ;main(); sub main { parseArguments(); my $certificate = retrieveCertificate(); my $x509 = Crypt::OpenSSL::X509->new_from_string( $certificate, Crypt::OpenSSL::X509::FORMAT_PEM ); decideExitCode( calculateDaysLeft($x509->notAfter()), extractCommonName($x509->subject()) ); } sub decideExitCode { my ( $daysLeft, $commonName ) = @_; if ( length ($ARG_COMMON_NAME) != 0 ) { if ( $ARG_COMMON_NAME ne $commonName ) { printf ("CRITICAL - Common name does not match. Got: %s\n" , $commonName); exit (2 ); } } if ( $daysLeft <= 0 ) { printf ("CRITICAL - Certificate is expired\n" ); exit (2 ); } if ( $daysLeft <= $ARG_CRITICAL_DAYS ) { printf ("CRITICAL - %d days left\n" , $daysLeft); exit (2 ); } elsif ( $daysLeft <= $ARG_WARNING_DAYS ) { printf ("WARNING - %d days left\n" , $daysLeft); exit (1 ); } else { printf ("OK - %d days left\n" , $daysLeft); exit (0 ); } } sub parseArguments { GetOptions ( 'address=s' => \$ARG_ADDRESS, 'port=s' => \$ARG_PORT, 'hostname=s' => \$ARG_HOSTNAME, 'common-name=s' => \$ARG_COMMON_NAME, 'file=s' => \$ARG_FILE, 'warn=i' => \$ARG_WARNING_DAYS, 'crit=i' => \$ARG_CRITICAL_DAYS, 'openssl=s' => \$ARG_OPENSSL, 'starttls=s' => \$ARG_STARTTLS ); validateArguments(); } sub validateArguments { if ( ! isInteger($ARG_WARNING_DAYS) ) { exitUnknown("Argument --warn is not numeric" ); } if ( ! isInteger($ARG_CRITICAL_DAYS) ) { exitUnknown("Argument --crit is not numeric" ); } if ( $ARG_CRITICAL_DAYS > $ARG_WARNING_DAYS ) { exitUnknown(sprintf ( 'Critical value (%d) is greater than warning value (%d)' , $ARG_CRITICAL_DAYS, $ARG_WARNING_DAYS )); } if ( length ($ARG_FILE) == 0 ) { validateNetworkArguments(); } else { validateFileArguments(); } } sub validateNetworkArguments { if ( ! -e $ARG_OPENSSL ) { exitUnknown(sprintf ("OpenSSL not found under %s" , $ARG_OPENSSL)); } if ( length ($ARG_ADDRESS) == 0 ) { exitUnknown("Argument --address is not set" ); } if ( ! isInteger($ARG_PORT) ) { exitUnknown("Argument --port is not numeric" ); } if ( $ARG_PORT <= 0 || $ARG_PORT >= 65535 ) { exitUnknown("Argument --port is out of bounds! Valid: 1-65535" ); } } sub validateFileArguments { if ( ! -e $ARG_FILE ) { exitUnknown(sprintf ( "Certificate under %s not found" , $ARG_FILE )); } } sub isInteger { my $possibleInteger = shift ; return 1 if ( $possibleInteger =~ /\d+/ ); return 0 ; } sub exitUnknown { printf ("UNKNOWN - %s\n" , shift ); exit 3 ; } sub retrieveCertificate { my $certificate = '' ; if ( length ($ARG_FILE) == 0 ) { my $sniPart = '' ; if ( length ($ARG_HOSTNAME) != 0 ) { $sniPart = sprintf ("-servername %s" , $ARG_HOSTNAME); } my $starttlsPart = '' ; if ( length ($ARG_STARTTLS) != 0 ) { $starttlsPart = sprintf ("-starttls %s" , $ARG_STARTTLS); } my $command = sprintf ( "echo \"\" | %s s_client -connect %s:%d %s %s 2> /dev/null | %s x509 2> /dev/null" , $ARG_OPENSSL, $ARG_ADDRESS, $ARG_PORT, $starttlsPart, $sniPart, $ARG_OPENSSL ); $certificate = `$command` ; } else { open (CERT, $ARG_FILE); my $line = '' ; while ( $line = <CERT> ) { $certificate .= $line; } close (CERT); } if ( $certificate !~ /BEGIN CERTIFICATE/ ) { exitUnknown("Didn't receive a certificate" ); } return $certificate; } sub calculateDaysLeft { my $expireDate = shift ; my ($year, $month, $day) = Parse_Date($expireDate); my ( $nowYear, $nowMonth, $nowDay) = Today(); return Delta_Days($nowYear, $nowMonth, $nowDay, $year, $month, $day); } sub extractCommonName { my $possibleCommonName = shift ; if ( $possibleCommonName =~ m/CN=(.*)/ ) { my $cn = $1; $cn =~ s/,.*// ; return $cn; } else { exitUnknown("No common name given" ); } }
Заранее обращаю внимание, что, при необходимости, можно изменить порог для проверки срока истечения сертификата:
our $ARG_WARNING_DAYS = 14; our $ARG_CRITICAL_DAYS = 7;
Шаг 3. Добавляем команду в Icinga2 В конфигурационный файл commands.conf
добавляем команду и ключи выполнения:
object CheckCommand "tls_certificate_expiration" { import "plugin-check-command" command = [ PluginDir + "/check_tls_certificate_expiration" ] arguments = { "--address" = "$tls_address $" "--port" = "$tls_port $" "--hostname" = "$tls_hostname $" "--common-name" = "$tls_common_name $" "--file" = "$tls_file $" "--warn" = "$tls_warn $" "--crit" = "$tls_crit $" "--openssl" = "$tls_openssl $" "--starttls" = "$tls_protocol $" } vars.tls_address = "$address $" }
Шаг 4. Добавляем сервис Добавляем сервис для мониторинга на примере домена моего блога (не забудьте, что обязательно должен быть указан объект хоста) :
object Host "bogachev.biz" { (...) } object Service "tls_bogachev.biz" { host_name = "bogachev.biz" display_name = "Certificate expires of BOGACHEV.BIZ" check_interval = 1d check_command = "tls_certificate_expiration" vars.tls_hostname = "bogachev.biz" }
Перезагружаем сервис Icinga2 и проверяем.
Если мы хотим использовать проверку для нескольких хостов, то используем apply Service
.
Для того, чтобы добавить сервисную группу для проверки TLS сертификата, то добавим следующие строки в конфигурационный файл groups.conf
object ServiceGroup "tls_certificate_expiration" { display_name = "TLS certificate expiration" assign where service.check_command == "tls_certificate_expiration" }
Проверка HTTPS сертификата через SNI (Server Name Indication)
object Service "tls-bogachev.biz" { import "generic-service" check_command = "tls_certificate_expiration" host_name = "bogachev.biz" vars.tls_hostname = "bogachev.biz" vars.tls_common_name = "bogachev.biz" // Note: address is automatically set to the host's address // Note: port is default 443 // Note: If you skip tls_common_name common name checking is disabled }
Для проверки локального сертификата, например для приложения, используем следующую команду:
object Service "tls-bogachev" { import "generic-service" check_command = "tls_certificate_expiration" host_name = "bogachev.local" vars.tls_file = "/path/to/CA.pem" // Note: address is automatically set to the host's address }