Когда ты имеешь несколько десятков сертификатов под своей ответственностью — это нормально. Часть из них будет продлеваться автоматически, а которые необходимо продлевать вручную, ты занесешь в календарь и поставишь напоминания. Пару раз в год на телефоне или рабочем ноутбуке будет выскакивать уведомление. Но если под твоей ответственностью тысячи сертификатов, да еще и на разных серверах и в разных филиалах — это чуть‑чуть усложняет задачу.

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

Так элементы данных выглядят в заббикс:

SANs и Issuer замазал
SANs и Issuer замазал

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