Мониторинг SSL сертификатов в Zabbix

Когда ты имеешь несколько десятков сертификатов под своей ответственностью — это нормально. Часть из них будет продлеваться автоматически, а которые необходимо продлевать вручную, ты занесешь в календарь и поставишь напоминания. Пару раз в год на телефоне или рабочем ноутбуке будет выскакивать уведомление. Но если под твоей ответственностью тысячи сертификатов, да еще и на разных серверах и в разных филиалах — это чуть‑чуть усложняет задачу.
Окей, возможно, ты гений автоматизации и у тебя все продлевается автоматически, а ты просто попиваешь кофе и наблюдаешь за тем, как работа работается сама. Но всегда что‑то может пойти не так, и мы должны быть уверены, что в этом случае в нашем любимом Zabbix загорится триггер бордового цвета, который будет мозолить глаза на всех мониторах и долетит до всех мессенджеров.
Если, конечно, ты не хочешь, чтобы тебя будили в 2 часа ночи, потому что коллега в другом городе — где рабочий день уже начался — не может подключиться к корпоративной сети по VPN. Потому что сертификат истёк.
Не так давно наши системные администраторы собрали сертификаты со всех CA в единую базу — Netbox. Им удобно: все в одном месте. Нам не менее удобно: не нужно лезть на каждый CA, чтобы вытянуть данные по сертификатам. Всего один API запрос, и все сертификаты на нашем zabbix proxy. Красотень.
Итак, была поставлена задача мониторить следующие ключи:
sans — список всех дополнительных доменных имён или IP, для которых действителен сертификат. Часто может быть пустым.
days_remaining — то, ради чего все это затевается.
valid_to — фактическая дата истечения срока действия.
fingerprint_sha256 — уникальный хеш сертификата.
issuer — кто подписал сертификат. В нашем случае он полезен тем, что показывает, на какой CA лезть. Если у вас только один CA, то это значение бесполезно, вы и так знаете, куда вам идти.
Отдельно хочу отметить:
common_name — мы не будем его хранить, а будем подставлять вместо sans, когда он пустой.
id — именно по нему будет работать правило обнаружения: каждый сертификат становится отдельным набором элементов данных в Zabbix.
Чтобы изучить, какие ключи есть у каждого сертификата, вы можете вытянуть один сертификат с помощью curl и изучить все ключи и их значения:
TOKEN=$(< /etc/zabbix/scripts/netbox.token)
curl -s -X GET \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/json" \
"https://example.com/api/plugins/ssl/certificates/?limit=1" \
| jq '.results[0]'Теперь давайте разберем структуру проекта на zabbix proxy:
/etc/zabbix/scripts/
├── certificates.txt
├── get_certificates.sh
├── netbox.token
├── ssl_sender.shcertificates.txt— сюда будем складывать все сертификаты.get_certificates.sh— скрипт, который тянет серты с нетбокса.netbox.token— файл с АПИ токеном и правами 600.ssl_sender.sh— этот скрипт отправит все данные в заббикс.
Перетягиваем данные из Netbox в Zabbix
Начнем с get_certificates.sh. Именно он заберет все сертификаты с нетбокса и аккуратненько сложит их в файлик.
#!/bin/bash
set -euo pipefail
TOKEN_FILE="/etc/zabbix/scripts/netbox.token"
[ -r "$TOKEN_FILE" ] || { echo "ERROR: $TOKEN_FILE not readable" >&2; exit 1; }
TOKEN=$(< "$TOKEN_FILE")
BASE_URL="https://example.com"
OUTPUT_FILE="/etc/zabbix/scripts/certificates.txt"
TMP_FILE="${OUTPUT_FILE}.tmp"
: > "$TMP_FILE"
chmod 600 "$TMP_FILE"
next_url="${BASE_URL}/api/plugins/ssl/certificates/?limit=100"
while [ -n "${next_url}" ] && [ "${next_url}" != "null" ]; do
response=$(curl -sf -X GET \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
"${next_url}") || { echo "ERROR: curl failed for $next_url" >&2; exit 1; }
echo "$response" | jq -c '.results[] |
select(.days_remaining >= 0) |
{
id: (.id | tostring),
sans: (if (.sans | length) > 0 then (.sans | join(" ")) else (.common_name // "") end),
days_remaining: .days_remaining,
valid_to: (.valid_to | split("T")[0] + " " + (split("T")[1] | .[0:5])),
fingerprint_sha256: .fingerprint_sha256,
issuer: (.issuer // "")
}' \
>> "$TMP_FILE" || { echo "ERROR: jq failed" >&2; exit 1; }
next_url=$(echo "$response" | jq -r '.next')
done
mv "$TMP_FILE" "$OUTPUT_FILE"Скрипт выполняет запрос в нетбокс, вытягивает оттуда все сертификаты с необходимыми ключами и кладет их в файл certificates.txt. Вот как выглядит один сертификат:
{“id”:“999”,“sans”:“my-test-server 10.20.30.40”,“days_remaining”:777,“valid_to”:“2030-01-15 08:22”,“fingerprint_sha256”:“11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00”,“issuer”:“CN=Test Root CA, O=Fake Company, C=XX”}
Когда вся база сертов у нас в руках, нужно все это дело как‑то отправить в заббикс. Хочу сказать, что изначально мы тупанули и создали скрипт, который подразумевал, что заббикс прокси будет сам вытягивать из файла все сертификаты через правило обнаружения и потом лезть за каждым значением. В нашей компании более 7000 сертификатов, умножьте их на 5 ключей, и вы получите более 35 000 элементов данных. Прокси начал задыхаться, а очередь из этих 35 000 значений выстроилась и не собиралась рассасываться.
Так мы пришли к оптимальному решению: zabbix trapper. Это просто чудо чудесное. Одна команда, одна секунда, и данные в вашем заббикс. Никакой нагрузки на прокси и никаких очередей.
А вот скрипт ssl_sender.sh, который выполняет всю работу по отправке:
#!/bin/bash
set -euo pipefail
CERT_FILE="/etc/zabbix/scripts/certificates.txt"
ZABBIX_SERVER="127.0.0.1"
ZABBIX_PORT="10051"
ZABBIX_HOST="SSL Monitoring"
[ -r "$CERT_FILE" ] || { echo "ERROR: $CERT_FILE not readable" >&2; exit 1; }
TMPDIR=$(mktemp -d /tmp/ssl_sender.XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT
DISCOVERY_FILE="$TMPDIR/discovery.txt"
METRICS_FILE="$TMPDIR/metrics.txt"
jq -s -c --arg h "$ZABBIX_HOST" '
{ data: [ .[] | { "{#ID}": .id, "{#SANS}": (.sans[0:150]) } ] }
' "$CERT_FILE" \
| jq -r --arg h "$ZABBIX_HOST" '"\"\($h)\" ssl.cert.discovery \(@json)"' \
> "$DISCOVERY_FILE"
jq -s -r --arg h "$ZABBIX_HOST" '
.[] |
"\"\($h)\" ssl.cert.days_left[\(.id)] \(.days_remaining)",
"\"\($h)\" ssl.cert.valid_to[\(.id)] \(.valid_to | @json)",
"\"\($h)\" ssl.cert.sans[\(.id)] \(.sans | @json)",
"\"\($h)\" ssl.cert.fingerprint[\(.id)] \(.fingerprint_sha256 | @json)",
"\"\($h)\" ssl.cert.issuer[\(.id)] \(.issuer | @json)"
' "$CERT_FILE" > "$METRICS_FILE"
zabbix_sender -z "$ZABBIX_SERVER" -p "$ZABBIX_PORT" -i "$DISCOVERY_FILE"
sleep 5
zabbix_sender -z "$ZABBIX_SERVER" -p "$ZABBIX_PORT" -i "$METRICS_FILE"Обратите внимание на ZABBIX_HOST. Хост с таким именем должен быть создан в вашем заббикс, чтобы данные корректно отправились. Также на него необходимо накинуть заббикс шаблон, который мы создадим позже.
jq -s -c --arg h "$ZABBIX_HOST" '
{ data: [ .[] | { "{#ID}": .id, "{#SANS}": (.sans[0:150]) } ] }
' "$CERT_FILE" \
| jq -r --arg h "$ZABBIX_HOST" '"\"\($h)\" ssl.cert.discovery \(@json)"' \
> "$DISCOVERY_FILE"Здесь мы указываем, по какому ключу у нас будет работать правило обнаружения. В нашем случае по ключу id. Также указываем имя ключа для правила обнаружения в нашем будущем шаблоне. Оно может быть любым. Ну, почти любым. Старайтесь соблюдать стандарты заббикс: разделяйте слова в ключе точками, как в нашем примере ssl.cert.discovery.
"\"\($h)\" ssl.cert.days_left[\(.id)] \(.days_remaining)",
"\"\($h)\" ssl.cert.valid_to[\(.id)] \(.valid_to | @json)",
"\"\($h)\" ssl.cert.sans[\(.id)] \(.sans | @json)",
"\"\($h)\" ssl.cert.fingerprint[\(.id)] \(.fingerprint_sha256 | @json)",
"\"\($h)\" ssl.cert.issuer[\(.id)] \(.issuer | @json)"А это прототипы элементов данных. ssl.cert.days_left — имя ключа в шаблоне заббикс. Оно также может быть любым, но должно совпадать в скрипте и в шаблоне. days_remaining — ключ в файле certificates.txt. Из этого ключа заббикс будет брать значение.
zabbix_sender -z "$ZABBIX_SERVER" -p "$ZABBIX_PORT" -i "$DISCOVERY_FILE"
sleep 5
zabbix_sender -z "$ZABBIX_SERVER" -p "$ZABBIX_PORT" -i "$METRICS_FILE"А здесь происходит отправка данных на сервер. Сначала срабатывает правило обнаружения: создаются все элементы данных. И только потом отправляются все значения. Между ними стоит sleep 5, но могу сказать, что это бесполезно, так как 5 секунд — это очень мало. Сами элементы данных то создадутся, но заббикс прокси не успеет получить о них информацию от сервера. Чтобы это правило работало корректно, нужно ставить слип на 3–5 минут. Но мы оставили так, как есть. Просто за первый проход скрипт отрабатывает дискавери, а за второй уже отправляет метрики. Нас это устраивает.
Создание шаблона
В первую очередь создаем новый шаблон. Мы его назвали SSL Certificates Monitoring trapper. И внутри шаблона создаем правило обнаружения.

ssl_sender.sh.После создания правила обнаружения создаем для него прототипы элементов данных.

В имя элементов данных мы вставили только ID, поскольку все остальное у нас будет отображаться в значениях. Сначала в имени у нас также содержался SANs, но впоследствии мы посчитали, что это лишнее. Имейте ввиду, что у имени элемента данных есть ограничение 250 символов. Если полное имя вместе с SANs и ID будет превышать эту длину, то элемент данных просто не создастся.
Теперь осталось создать триггеры.

Как вы можете заметить, мы сделали три триггера:
Средняя важность, если сертификату осталось жить меньше 28 дней
Высокая важность, если сертификату осталось жить меньше 14 дней
Чрезвычайная важность, если сертификату осталось жить меньше 2 дней
Осталось создать хост и накинуть на него данный шаблон.

Здесь самое важное, чтобы имя узла совпадало с тем, которое прописано у нас в скрипте ssl_sender.sh. В нашем случае это SSL Monitoring. Также правильно укажите адрес агента.
Отправка данных в Zabbix
Вся основная часть готова, осталось запустить скрипт ssl_sender.sh и дождаться результата. Не забудьте дать всем скриптам права на исполнение. После запуска скрипта, вы должны увидеть следующее:
zabbix@zabbixproxy:/etc/zabbix/scripts# ./ssl_sender.sh
Response from "127.0.0.1:10051": "processed: 1; failed: 0; total: 1; seconds spent: 0.008893"
sent: 1; skipped: 0; total: 1
Response from "127.0.0.1:10051": "processed: 250; failed: 0; total: 250; seconds spent: 0.002481"
sent: 250; skipped: 0; total: 250Сначала срабатывает правило обнаружения. Потом срабатывает отправка метрик. Все успешно. 1 правило, 250 метрик. У меня этот скрипт уже отрабатывал, поэтому все прошло без ошибок. Если вы будете запускать впервые, то скорее всего правило обнаружения отработает успешно, а метрики упадут в ошибку. Об этом я писал выше. Просто запустите скрипт повторно через 3–5 минут.
Так элементы данных выглядят в заббикс:

Осталось создать расписание запуска скрипта. Я лично почти всегда создаю отдельный юнит, но данный процесс мы уже разбирать не будем. Всем удачи и поменьше красных триггеров.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.