Когда ты имеешь несколько десятков сертификатов под своей ответственностью — это нормально. Часть из них будет продлеваться автоматически, а которые необходимо продлевать вручную, ты занесешь в календарь и поставишь напоминания. Пару раз в год на телефоне или рабочем ноутбуке будет выскакивать уведомление. Но если под твоей ответственностью тысячи сертификатов, да еще и на разных серверах и в разных филиалах — это чуть‑чуть усложняет задачу.
Окей, возможно, ты гений автоматизации и у тебя все продлевается автоматически, а ты просто попиваешь кофе и наблюдаешь за тем, как работа работается сама. Но всегда что‑то может пойти не так, и мы должны быть уверены, что в этом случае в нашем любимом 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.sh
certificates.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 минут.
Так элементы данных выглядят в заббикс:

Осталось создать расписание запуска скрипта. Я лично почти всегда создаю отдельный юнит, но данный процесс мы уже разбирать не будем. Всем удачи и поменьше красных триггеров.
