UMGUM.COM 

Zabbix + SSL-certificate check ( Отслеживаем истечение срока действия SSL/TLS-сертификатов для типичного набора интернет-сервисов. )

5 октября 2017  (обновлено 23 марта 2021)

OS: "Linux Debian/Ubuntu".
Application: "Zabbix v2/3/4/5", "OpenSSL", "Bash".

Задача: наладить посредством системы мониторинга "Zabbix" отслеживание истечения срока действия SSL/TLS-сертификатов для типичного набора интернет-сервисов, вроде сайтов, почтовых (SMTP,POP3,IMAP) и FTP/LDAP-серверов.

Я предпочитаю выносить мониторинг доступности сайтов отдельно от мониторинга несущих таковые серверов. Прежде всего, мы не всегда управляем серверам сайтов, состояние которых должны отслеживать - потому в большой инфраструктуре со временем будет нарастать путаница, когда часть сайтов мы отслеживаем в разрезе несущих их серверов, а часть как нечто несамостоятельное, через вспомогательных агентов. Также сайты иногда переносятся между серверами, а значит придётся переносить и настройки мониторинга - что привносит лишнюю работу и неминуемо нарастает технический долг отложенных и забытых изменений.

Таким образом, довольно скоро я пришёл к тому, что сайты лучше мониторить как отдельные сущности, наряду с серверами и сетевым оборудованием. То есть, для каждого сайта я создаю отдельную запись "Host", именуя её как-то вроде "Web: www.sitename.example.net".

Такой подход, с выносом мониторинга web-сервиса в "псевдо-хосты" заодно подводит нас к удобному способу группировки объектов мониторинга, с указанием на тип сервиса, вроде "https://", "smtps://", "ftps://", "ldaps://" или "streams://".

Разумеется, для реализации задачи мониторинга "псевдо-хостам" нужно будет выделить агента исполнения сценариев. Мне представляется естественным повесить эту задачу на агента самого сервера "Zabbix", если у такового имеется возможность достучаться до всех удалённых сетевых узлов, разумеется.


Предварительная подготовка.

На стороне сервера "Zabbix" должен быть установлен "Zabbix Agent" и "OpenSSL":

# apt-get install bash openssl coreutils

Мною подготовлен специализированный шаблон для системы мониторинга - его надо применить на сервере "Zabbix":


О подготовке шаблона (template).

В составлении самодельного шаблона ничего сложного нет, и здесь я отмечу лишь несколько ключевых моментов.

Прежде всего, назовём шаблон "Template App SSL Certificate", следуя общему правилу именований в "Zabbix". Набор отслеживаемых параметров сведём в группу (называемую в этой системе мониторинга как "Application") "SSL/TLS service".

В создаваемом шаблоне логику запросов и триггеров поставим в зависимость от следующих переменных, которые потребуется переопределить (при необходимости) в настройках мониторинга каждой отдельной сущности:

{$SSL_HOST_FQDN} => (empty)
{$SSL_HOST_PORT} => 443
{$SSL_HOST_MODE} => native
{$SSL_SNI_FQDN} => (empty)

Набор отслеживаемых параметров с говорящими наименованиями:

<item>
  <name>Certificate issuer</name>
  <key>ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},issuer,{$SSL_SNI_FQDN}]</key>
  <units>days</units>

<item>
  <name>Certificate subject (name)</name>
  <key>ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},subject,{$SSL_SNI_FQDN}]</key>
  <units>days</units>

<item>
  <name>Certificate fingerprint</name>
  <key>ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},fingerprint,{$SSL_SNI_FQDN}]</key>
  <units>days</units>

<item>
  <name>Сertificate expiration date</name>
  <key>ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},enddate,{$SSL_SNI_FQDN}]</key>
  <units>days</units>

<item>
  <name>Days left before the certificate expires</name>
  <key>ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},lifetime,{$SSL_SNI_FQDN}]</key>
  <units>days</units>

Примеры триггеров, посредством которых мы будем информироваться об изменениях состояния SSL/TLS-сертификатов:

<trigger>
  <name>Certificate fingerprint on {HOST.NAME} changed
  <Expression>{Template App SSL Certificate:ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},fingerprint,{$SSL_SNI_FQDN}].diff()}=1

<trigger>
  <name>Certificate subject on {HOST.NAME} is unreachable for one day
  <Expression>{Template App SSL Certificate:ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},subject,{$SSL_SNI_FQDN}].nodata(1d)}=1

<trigger>
  <name>Certificate on {HOST.NAME} will expire in ten days
  <Expression>{Template App SSL Certificate:ssl.check[{$SSL_HOST_FQDN}:{$SSL_HOST_PORT},{$SSL_HOST_MODE},lifetime,{$SSL_SNI_FQDN}].last()}<11

Настройка "Zabbix Agent".

Учитывая то, что процедуры сбора данных осуществляется агентом "Zabbix", то конфигурационные файл и скрипты расположим в его директории:

# mkdir -p /etc/zabbix/zabbix_agents.conf.d

Определяем параметры "Zabbix Agent"-а, запросы к которым будут обслуживаться внешними приложениями:

# vi /etc/zabbix/zabbix_agentd.conf.d/ssl_check.conf

UserParameter=ssl.check[*], /etc/zabbix/scripts/ssl_check.sh $1 $2 $3 $4

Установленной по умолчанию двухсекундной задержки при ожидании ответа от "Zabbix Agent"-а на практике недостаточно - продлеваем до пяти секунд:

# vi /etc/zabbix/zabbix_agentd.conf

....
### Option: Timeout
Timeout=5
....

Профилактически закрываем конфигурационные файлы от доступа посторонних:

# chown -R zabbix:zabbix /etc/zabbix
# chmod o-rwx /etc/zabbix

Для применения изменений в конфигурации "Zabbix Agent" необходимо перезапустить:

# /etc/init.d/zabbix-agent restart

Подготавливаем в файловой системе место для хранения журналов событий:

# mkdir -p /var/log/zabbix-agent
# chown -R zabbix:zabbix /var/log/zabbix-agent
# chmod o-rwx /var/log/zabbix-agent

Пишем и тестируем скрипт получения запрашиваемых параметров.

Создаём специализированный bash-скрипт, принимающий от "Zabbix Agent"-а запросы на получение данных по ряду интересующих нас параметров, обращающийся посредством CLI-утилиты "openssl" к целевым серверам и нормализующий ответ перед выдачей:

# mkdir -p /etc/zabbix/scripts
# vi /etc/zabbix/scripts/ssl_check.sh

#!/bin/bash

# USAGE:
#   Command      ARG1      ARG2     ARG3   ARG4
#   ssl_check.sh HOST:PORT SNI_NAME METHOD CHECK_TYPE

# usage: ./ssl_check.sh "hostName:port" "sniName" "methodSSL" "checkType"

# Задаём переменные рабочего окружения
OPENSSL="/usr/bin/openssl"
OPENSSL_STARTTLS_PROTOS="smtp pop3 imap ftp ldap"
STARTTLS=""
X509_OPT=""
#
# Задаём параметры принудительного органичения длительности запроса
TIMEOUT_EXEC="/usr/bin/timeout"
TIMEOUT=5 # (in seconds)
#
# Указываем месторасположение журнала и фиксируем дату события
LOG="/var/log/zabbix-agent/zabbix-ssl-check-error.log"
DATE=$(date +"%Y-%m-%d.%H:%M:%S")

# Проверяем наличие ожидаемых утилит
[ -x "$(command -v openssl)" ] && [ -x "$(command -v timeout)" ] || { echo "${DATE}: Necessary utilities not found." >> ${LOG}; exit 1; }

# Принимаем аргументы
HOST_AND_PORT="${1}" # (IP:PORT or FQDN:PORT of host server address)
HOST_METHOD="${2}" # ("native" or proto of StartTLS)
QUERY_TYPE="${3}" # ("issuer", "subject", "fingerprint", "enddate", "lifetime")
SNI_FQDN="${4}" # (FQDN of the site being checked)
[ -z "${SNI_FQDN}" ] && { SNI_FQDN=$(echo ${HOST_AND_PORT} | awk -F ":" '{print $1}'); }

# (опционально) Формируем строку параметров запроса "StartTLS"
if [ "${HOST_METHOD}" != "native" ] ; then
  for PROTO in ${OPENSSL_STARTTLS_PROTOS} ; do
    [ "${HOST_METHOD}" == "${PROTO}" ] && { STARTTLS="-starttls ${PROTO}" ; break; }
  done
fi

# Осуществляем запросы сертификата и вычленяем интересующие нас параметры
if [ "${QUERY_TYPE}" == "issuer" ] ; then

  # Получаем имя "родительского" сертификата
  X509_OPT="-issuer"
  ANSWER=$(echo | ${TIMEOUT_EXEC} ${TIMEOUT} ${OPENSSL} s_client -connect ${HOST_AND_PORT} -servername ${SNI_FQDN} ${STARTTLS} 2>/dev/null | ${OPENSSL} x509 -noout ${X509_OPT} 2>/dev/null)
  ANSWER=${ANSWER#*issuer=}

elif [ "${QUERY_TYPE}" == "subject" ] ; then

  # Получаем полное имя сертификата
  X509_OPT="-subject"
  ANSWER=$(echo | ${TIMEOUT_EXEC} ${TIMEOUT} ${OPENSSL} s_client -connect ${HOST_AND_PORT} -servername ${SNI_FQDN} ${STARTTLS} 2>/dev/null | ${OPENSSL} x509 -noout ${X509_OPT} 2>/dev/null)
  ANSWER=${ANSWER#*subject=}

elif [ "${QUERY_TYPE}" == "fingerprint" ] ; then

  # Получаем "отпечаток" открытого ключа (своего рода короткий уникальный идентификатор)
  X509_OPT="-fingerprint"
  ANSWER=$(echo | ${TIMEOUT_EXEC} ${TIMEOUT} ${OPENSSL} s_client -connect ${HOST_AND_PORT} -servername ${SNI_FQDN} ${STARTTLS} 2>/dev/null | ${OPENSSL} x509 -noout ${X509_OPT} 2>/dev/null)
  ANSWER=${ANSWER#*Fingerprint=}

elif [ "${QUERY_TYPE}" == "enddate" ] ; then

  # Получаем время и дату завершения срока действия сертификата
  X509_OPT="-enddate"
  ANSWER=$(echo | ${TIMEOUT_EXEC} ${TIMEOUT} ${OPENSSL} s_client -connect ${HOST_AND_PORT} -servername ${SNI_FQDN} ${STARTTLS} 2>/dev/null | ${OPENSSL} x509 -noout ${X509_OPT} 2>/dev/null)
  ANSWER=${ANSWER#notAfter=}

elif [ "${QUERY_TYPE}" == "lifetime" ] ; then

  # Получаем количество дней до завершения срока действия сертификата
  X509_OPT="-enddate"
  ANSWER=$(echo | ${TIMEOUT_EXEC} ${TIMEOUT} ${OPENSSL} s_client -connect ${HOST_AND_PORT} -servername ${SNI_FQDN} ${STARTTLS} 2>/dev/null | ${OPENSSL} x509 -noout ${X509_OPT} 2>/dev/null)
  ANSWER=${ANSWER#notAfter=}
  EXPIRE_UNIXTIME_SECS=`date -d "${ANSWER}" +%s`
  EXPIRE_DIFF_SECS=$(( ${EXPIRE_UNIXTIME_SECS} - `date +%s` ))
  ANSWER=$(( ${EXPIRE_DIFF_SECS} / 24 / 3600 ))

fi

# Отдаём значение запрашиваемого параметра (или ничего не делаем если запрашиваемый параметр отсутствует)
[ ! -z "${ANSWER}" ] && echo ${ANSWER}

echo "${DATE}:" >> "${LOG}"
echo "* ${HOST_AND_PORT}" >> "${LOG}"
echo "* ${HOST_METHOD}" >> "${LOG}"
echo "* ${QUERY_TYPE}" >> "${LOG}"
echo "* ${SNI_FQDN}" >> "${LOG}"
echo "${ANSWER}" >> "${LOG}"
echo "" >> "${LOG}"

exit ${?}

Переводим файл во владение "Zabbix Agent"-а и делаем его исполняемым:

# chown zabbix:zabbix ./ssl_check.sh && chmod ug+x,o-rwx ./ssl_check.sh

Осуществляем тестовый запрос непосредственно с несущего сервера, без участия "Zabbix":

$ ./ssl_check.sh site.example.net:443 site.example.net native simple

Наладка ротации файлов журналов событий web-сайтов.

Более для наладки процесса в скрипте выше на каждый запрос в журнале событий делается запись. Если забыть об этом, то через полгода в файловой системе вырастет огромный ненужный файл. Заранее натравим на него подсистему усечения и оборота журналов:

# vi /etc/logrotate.d/zabbix-agent

....

/var/log/zabbix-agent/*.log {
  weekly
  rotate 7
  compress
  delaycompress
  missingok
  notifempty
  copytruncate
  su zabbix zabbix
}

Проверяем корректность конфигурации, не воздействуя при этом на файлы журналов:

# logrotate -d /etc/logrotate.d/zabbix-agent


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )