Авторы статьи: Артем Зубков, Junior администратор отдела DevOps.

В современных распределённых системах надёжность и безопасность инфраструктуры напрямую зависят от корректного функционирования криптографических компонентов, в частности — SSL/TLS-сертификатов. Одним из критически важных аспектов эксплуатации таких систем является своевременный мониторинг срока действия сертификатов, поскольку их просрочка может привести к нарушению работы сервисов, недоступности API, сбоям в аутентификации и даже компрометации безопасности соединений. В рамках экосистемы oVirt Engine, выступающего центральным узлом управления виртуальной инфраструктурой, особое значение имеют сертификаты, обеспечивающие защищённое взаимодействие между компонентами системы и конечными пользователями.

Серверы для мониторинга

Закажите сервер с предустановленным ПО для мониторинга: Grafana, Zabbix, Prometheus и другими.

Узнать больше

В данной статье мы рассмотрим практику реализации автоматизированного мониторинга срока действия двух ключевых сертификатов: apache.cer и websocket-proxy.cer, размещённых на каждом oVirt Engine в директории /etc/pki/ovirt-engine/certs/

Сертификат apache.cer используется веб-сервером Apache, который обслуживает веб-интерфейс и REST API oVirt Engine, обеспечивая шифрование и аутентификацию клиентских подключений. В свою очередь, websocket-proxy.cer применяется для защиты WebSocket-соединений, необходимых для передачи консольных сессий виртуальных машин через браузер. Несвоевременное обновление этих сертификатов может привести к недоступности управления виртуальными машинами и административного интерфейса, что делает их мониторинг приоритетной задачей.

Для решения этой задачи мы разработали специализированный экспортер — cert_checker, размещаемый непосредственно на каждом oVirt Engine в каталоге /opt/cert_checker. Для тех, кто не знает, oVirt Engine — это центральный сервер управления, который контролирует все ноды виртуализации, общие дисковые ресурсы и виртуальные сети.

Экспортер анализирует указанные файлы сертификатов, извлекает дату их истечения и предоставляет метрики в формате, совместимом с Prometheus. Это позволяет интегрировать процесс мониторинга в существующую систему сбора и визуализации метрик, настроить алертинг за несколько дней до окончания срока действия сертификатов и оперативно реагировать на потенциальные инциденты. 

Для корректной работы экспортера требуется наличие на хосте установленной версии Go (golang) не ниже той, которая использовалась при его компиляции, что гарантирует стабильность и совместимость исполняемого файла. Далее в статье будет подробно описана архитектура решения, процесс развёртывания, формат выдаваемых метрик и рекомендации по интеграции в инфраструктуру мониторинга.

Создание сервиса

Для работы экспортера так же необходимо создать сервис systemd. Для это идем в каталог /etc/systemd/system/ и создаем systemd-unit:

Description=oVirt cert cheker service
ConditionPathExists=/opt/cert_cheker
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/cert_cheker
ExecStart=/usr/local/go/bin/go run main.go //указать путь установки go и способ выполнения main.go
User=root
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target

После чего сделать

systemctl daemon-reload
systemctl start cert_cheker.service 

и проверить статус работы сервиса

systemctl status cert_cheker.service

В случае успеха вывод статуса должен выглядеть примерно так:

И наконец, нужно убедиться, что нужный нам порт (порт 1337), куда будут отдаваться метрики, не закрыт фаерволлом.

Для этого нужно ввести, например, команду 

netstat -na | grep 1337

Если вывод имеет следующий вид.:

Тогда необходимо открыть порт вручную, введя команду

firewall-cmd --add-port 1337/tcp

Проверяем результат:

Если порт открыт, следует проверить, тянется ли метрика. Для этого нужно ввести новую команду

curl http://localhost:1337/metrics 

и получить вывод, похожий на тот, что изображен на скриншоте ниже:

Создание алерта Prometheus

Первым делом создаем алерт. Все примеры мы будем показывать ссылаясь на наш gitlab и расположение Ansible плейбуков для развертывания инфраструктуры в нем. В случае Prometheus, его алерты можно найти по по пути /devops/ansible-playbooks/prometheus_playbook/ files/alerts

Выбираем раздел меню New file и создаем файл в формате yml:

Затем пишем алерт, где указываем имя самого алерта, нужный для мониторинга атрибут + значение, на которое будет срабатывать алерт, время обновления, тип алерта (в нашем примере warning), а также описание. Формат следующий:

groups:
- name: ovirt_engine_apache_cert_expiry
  rules:
    - alert: ovr_apache_cert_expiry
      expr: ovirt_engine_apache_cert_expiry < 14
      for: 45s
      labels:
        severity: warning
      annotations:
        summary: "Certificate for {{ $labels.engine }} will expire soon"
        description: "The certificate for {{ $labels.engine }} will expire in {{ $value }} days. Please renew it."
- name: ovirt_engine_ws_proxy_cert_expiry
  rules:
    - alert: ovr_ws_proxy_cert_expiry
      expr: ovirt_engine_ws_proxy_cert_expiry < 14
      for: 45s
      labels:
        severity: warning
      annotations:
        summary: "Certificate for {{ $labels.engine }} will expire soon"
        description: "The certificate for {{ $labels.engine }} will expire in {{ $value }} days. Please renew it."

Сохраняем и переходим в каталог /devops/ansible-playbooks/prometheus_playbook/group_vars. Нас интересует файл federation_lang.yaml. Открываем его через web IDE и в верхней части документа, рядом с остальными таргетами, ниже создаем свой:

В таргете прописаны имя таргета, путь на каталог на HTTP, поднятом ранее на 1337 порту. Далее, после static_configs, прописываем целевые адреса за портом 1337. Затем, после labels, название сервиса и компонент virtualization.

Так же необходимо в самом начале документа, после поля rule_files указать имя написанного алерта:

После этого переходим к Jenkins задаче conf_prometheus.dsl и запускаем обновление fideration_lang:

После успешного обновления следует перейти по URL (http://<ip>:9090/) и проверить отработку таргета, введя имя искомого атрибута, например ovirt_engine_apache_cert_expiry. Если проблем не возникло, то вывод будет примерно таким:

Вывод алерта на дэшборд Grafana

Если предыдущие шаги выполнены успешно, алерт будет выведен на общий дэшборд. Необходимо перенести его на дэшборд oVirt. Делается это следующим образом:

Заходим в настройки панели, выбрав пункт меню Edit:

Вписываем название алерта в формате job!="cert_cheker". Обязательно после должна быть запятая. Если название вписывается в середине поля, то запятую ставим с обеих сторон:

Далее идем в дэшборд Prometheus AlertManager - Ovirt checks:

На дэшборде oVirt снова заходим в настройке панели:

Жмем + Query :

В появившемся поле вписываем имя алерта опять в формате job="cert_cheker".

После чего нажимаем Save в верхней части страницы:

Как следствие, после выполненных действий алерт будет выводиться на дэшборд oVirt, и настройку можно считать оконченной.

Описание работы кода экспортера

Для начала импортируются необходимые пакеты, включая crypto/x509 для работы с сертификатами.  Пакет crypto/x509 имеет ключевое значение, поскольку предоставляет функции для парсинга сертификатов X.509 и проверки срока действия сертификатов.

Также необходимы пакеты encoding/pem для декодирования PEM-закодированных данных, io для работы с вводом-выводом, log для логирования, net/http для работы с HTTP-запросами, os для работы с операционной системой и time для работы со временем.

Еще необходимо импортировать пакеты из внешних библиотек:

  • github.com/prometheus/client_golang/prometheus для работы с Prometheus;

  • github.com/prometheus/client_golang/prometheus/promhttp для обработки HTTP-запросов Prometheus:

package main
import (
    "crypto/x509"
    "encoding/pem"
    "io"
    "log"
    "net/http"
    "os"
    "time"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

Дальше определяются две метрики: apacheCertExpiry и wsProxyCertExpiry, которые представляют собой измерители (gauges) в Prometheus. Они используются для отслеживания количества дней до истечения срока действия сертификатов Apache и WebSocket Proxy соответственно:

var (
    apacheCertExpiry = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "ovirt_engine_apache_cert_expiry",
        Help: "Number of days until the Apache certificate expires",
    })
    wsProxyCertExpiry = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "ovirt_engine_ws_proxy_cert_expiry",
        Help: "Number of days until the WebSocket Proxy certificate expires",
    })
)

В функции init() эти метрики регистрируются в Prometheus с помощью функции prometheus.MustRegister():

func init() {
    prometheus.MustRegister(apacheCertExpiry)
    prometheus.MustRegister(wsProxyCertExpiry)
}

В функции main() запускается HTTP-сервер (в данном случае на порту 1337), который будет принимать метрики в каталог /metrics, в формате который поддерживает Prometheus:

func main() {
    log.Println("Starting HTTP server on :1337")
    http.Handle("/metrics", promhttp.Handler())
    go func() {
        log.Fatal(http.ListenAndServe(":1337", nil))
    }()

Далее в бесконечном цикле каждый час проверяется срок действия сертификатов Apache и WebSocket Proxy с помощью функции checkCertExpiry(). Эта функция принимает путь к файлу сертификата в качестве аргумента и возвращает количество дней до истечения срока действия сертификата. Если количество дней до истечения срока действия сертификата больше или равно 0, значение метрики обновляется с помощью функции Set() :

for {
        apacheDaysUntilExpiry := checkCertExpiry("/etc/pki/ovirt-engine/certs/apache.cer")
        if apacheDaysUntilExpiry >= 0 {
            apacheCertExpiry.Set(float64(apacheDaysUntilExpiry))
        }
        wsProxyDaysUntilExpiry := checkCertExpiry("/etc/pki/ovirt-engine/certs/websocket-proxy.cer")
        if wsProxyDaysUntilExpiry >= 0 {
            wsProxyCertExpiry.Set(float64(wsProxyDaysUntilExpiry))
        }
        time.Sleep(1 * time.Hour)
    }
}

Если срок действия сертификата не удалось определить, тогда возвращается -1:

func checkCertExpiry(certFile string) int {
    log.Printf("Checking certificate %s\n", certFile)
    file, err := os.Open(certFile)
    if err != nil {
        log.Printf("Failed to open certificate %s: %v", certFile, err)
        return -1
    }
    defer file.Close()
    certData, err := io.ReadAll(file)
    if err != nil {
        log.Printf("Failed to read certificate %s: %v", certFile, err)
        return -1
    }
    block, _ := pem.Decode(certData)
    if block == nil {
        log.Printf("Failed to decode PEM block for %s", certFile)
        return -1
    }
    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        log.Printf("Failed to parse certificate %s: %v", certFile, err)
        return -1
    }
    daysUntilExpiry := int(cert.NotAfter.Sub(time.Now()).Hours() / 24)
    log.Printf("Certificate %s expires in %d days\n", certFile, daysUntilExpiry)
    return daysUntilExpiry
}

В функции checkCertExpiry() сначала открывается файл сертификата с помощью функции os.Open(). Потом данные сертификата читаются из файла с помощью функции io.ReadAll(). Далее данные сертификата декодируются из формата PEM с помощью функции `pem.Decode(). Затем данные сертификата парсятся в структуру x509.Certificate с помощью функции x509.ParseCertificate().

После этого вычисляется количество дней до истечения срока действия сертификата путем вычитания текущего времени из времени истечения срока действия сертификата с помощью функции Sub() и преобразования результата в дни. Количество дней до истечения срока действия сертификата возвращается функцией checkCertExpiry().

Итог

В ходе реализации мониторинга SSL/TLS-сертификатов в экосистеме oVirt мы создали надёжное и автоматизированное решение на основе самописного экспортера cert_checker. Этот инструмент позволяет в режиме реального времени отслеживать сроки истечения ключевых сертификатов — apache.cer и websocket-proxy.cer, — предотвращая потенциальные простои в работе веб-интерфейса и консольных подключений к виртуальным машинам. 

Интеграция с Prometheus и Grafana обеспечила нам централизованное наблюдение, а настройка алертинга позволила оперативно реагировать на приближающееся окончание срока действия сертификатов — за 14 дней до истечения, а не в последний момент. 

Полностью автоматизированный путь от сбора метрик до визуализации и оповещений повысил отказоустойчивость инфраструктуры и снизил операционные риски, связанные с человеческим фактором (а следовательно, повысил качество оказываемых нами услуг).

С учетом нескольких месяцев эксплуатации, пока «причесывалась» эта статья, можно констатировать следующее: решение оказалось простым, но эффективным: благодаря использованию стандартных библиотек Go и экосистемы Prometheus, мы получили гибкий и легко поддерживаемый компонент, который можно быстро адаптировать под другие типы сертификатов или системы. Важно, что теперь мониторинг стал проактивным — вместо реагирования на инциденты мы можем предотвращать их заранее.


А как вы организуете мониторинг сертификатов в своей инфраструктуре?

Серверы для мониторинга

Закажите сервер с предустановленным ПО для мониторинга: Grafana, Zabbix, Prometheus и другими.

Узнать больше


Комментарии (6)


  1. CrazyHackGUT
    10.09.2025 16:03

    А почему на хост, где поднимается экспортёр, доставлен код сервиса, запускаемый напрямую через go run, а не скомпилированный бинарник?


    1. Sly_tom_cat
      10.09.2025 16:03

      +100500 - там мало того что каждый раз компиляция + запуск, так еще и полная поддержка go должна быть в образе сервиса - что по идее просто нафиг не нужно, и весит на пару порядков больше чем скомпилированный go-шный бинарик.


  1. Sly_tom_cat
    10.09.2025 16:03

    А не проще ли было автоматизировать выпуск новых сертов и в метрики писать если их выпустить не удалось?


  1. Sly_tom_cat
    10.09.2025 16:03

    Да и gauge - не самый удобный метод. Можем пропустить всплески (сервис упал после увеличения метрики и при перезапуске метрика обнулилась). Если перезапуск произошел за меньше чем за 45 секунд (а для go - это не проблема) - алерт просто пропущен.

    Лучше использовать постоянно растущую метрику и триггер писать на диф от предыдущего значения. Там падение сервиса метрику тоже обнулит, но Prometeus умеет понимать, что обнуления не сбрасывают то, что выросло ранее. Для него сброс постоянно растущей метрики в 0 - это 0-вое приращение, что не обнуляет приращение отловленное ранее.


  1. Sly_tom_cat
    10.09.2025 16:03

    Сервис с таким: `for { bla-bla(); slep(time.Hour) }` можно только SIGKILL убить.
    Автору кода надо изучить вопрос gracefull shutdown (в go - решается элементарно).


  1. oomk
    10.09.2025 16:03

    А как вы организуете мониторинг сертификатов в своей инфраструктуре?

    Лучше это делать blackbox мониторингом, подключаясь на порт. Например cloudprober. Не знаю как вы обновляете сертификаты, но после появления нового файла сертификата на сервере, его еще процесс должен перечитать. Blackbox пробер проверит сразу конечный результат вместе с доступностью сервиса.