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)
{$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>
<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
<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
....
### Option: Timeout
Timeout=5
....
Профилактически закрываем конфигурационные файлы от доступа посторонних:
# chown -R zabbix:zabbix /etc/zabbix
# chmod o-rwx /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
# 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
# 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 ${?}
# 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
}
/var/log/zabbix-agent/*.log {
weekly
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
su zabbix zabbix
}
Проверяем корректность конфигурации, не воздействуя при этом на файлы журналов:
# logrotate -d /etc/logrotate.d/zabbix-agent