Контроль срока действия TLS сертификата с помощью Icinga2

Отслеживаем количество дней до истечения срока сертификата 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 со следующим содержимым:

#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use Date::Calc qw(Delta_Days Parse_Date Today);
use Crypt::OpenSSL::X509;
# Basic Arguments
our $ARG_WARNING_DAYS = 14;
our $ARG_CRITICAL_DAYS = 7;
# Arguments for network check
our $ARG_ADDRESS = '';
our $ARG_HOSTNAME = ''; # Only used for HTTP SNI
our $ARG_PORT = 443;
our $ARG_OPENSSL = '/usr/bin/openssl';
our $ARG_STARTTLS = '';
# Argument for file check
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 ) {
# Common name check wanted
if ( $ARG_COMMON_NAME ne $commonName ) {
printf("CRITICAL - Common name does not match. Got: %s\n",
$commonName);
exit(2);
}
}
# Check if certificate is already expired
# Display an appropriate string then
if ( $daysLeft <= 0 ) {
printf("CRITICAL - Certificate is expired\n");
exit(2);
}
# Days left check
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 {
# Common Arguments
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
));
}
# Decide which mode
if ( length ($ARG_FILE) == 0 ) {
# Network mode
# Validate network parameter
validateNetworkArguments();
}
else {
# File mode
# Validate file parameter
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 ) {
# Network mode
# Fetch certificate and return it as a string
# Check if we have to set SNI
my $sniPart = '';
if ( length($ARG_HOSTNAME) != 0 ) {
$sniPart = sprintf("-servername %s", $ARG_HOSTNAME);
}
# Check if we have to set starttls protocol
my $starttlsPart = '';
if ( length($ARG_STARTTLS) != 0 ) {
$starttlsPart = sprintf("-starttls %s", $ARG_STARTTLS);
}
# Build command
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 {
# File mode
# Fetch certificate from file and return it as string
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=(.*)/ ) {
# Strip possible E-Mail address
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
}
Поделиться Комментарии