Три просроченных сертификата за квартал: автоматизируем SSL для 45 сайтов

Задача клиента: сертификаты истекают незаметно

В марте 2026 года к нам обратилось digital-агентство WebCraft из Казани. Агентство создавало и обслуживало сайты для малого и среднего бизнеса: 45 клиентских сайтов на 3 серверах (2 с nginx, 1 с Apache) плюс 8 сайтов в Kubernetes-кластере.

За последний квартал произошло три инцидента с просроченными SSL-сертификатами:

  • Инцидент 1: Интернет-магазин мебели — сертификат истёк ночью, утром покупатели увидели ERR_CERT_DATE_INVALID. Продажи встали на 4 часа до прихода администратора
  • Инцидент 2: Корпоративный сайт строительной компании — Chrome заблокировал переход на сайт. Клиент решил, что сайт взломали, и позвонил в панике
  • Инцидент 3: API-сервер мобильного приложения — приложение перестало работать у 15 000 пользователей, потому что pinned certificate не совпадал
«Каждый инцидент — это потерянные клиенты и нервный звонок от заказчика. Мы не можем вручную следить за 45 сертификатами, нужна автоматизация» — технический директор WebCraft.

Причина проблем: часть сертификатов была от Let's Encrypt (90 дней), часть — коммерческие (1 год). Обновление делалось вручную, сроки записывались в Google-таблицу, которую никто не проверял. Инженеры АйТи Фреш предложили комплексное решение: автоматическое обновление всех сертификатов + мониторинг с алертами.

Инвентаризация сертификатов

Первым делом мы провели инвентаризацию всех SSL-сертификатов на серверах клиента:

#!/bin/bash
# ssl-inventory.sh — инвентаризация всех SSL-сертификатов

echo "=== Инвентаризация SSL-сертификатов ==="
echo ""

# Сертификаты certbot
echo "--- Let's Encrypt (certbot) ---"
sudo certbot certificates 2>/dev/null | grep -E '(Certificate Name|Domains|Expiry Date)'

# Сертификаты в nginx
echo ""
echo "--- nginx конфиги ---"
for conf in /etc/nginx/sites-enabled/*; do
    CERT=$(grep -oP 'ssl_certificate\s+\K[^;]+' "$conf" 2>/dev/null | head -1)
    SERVER=$(grep -oP 'server_name\s+\K[^;]+' "$conf" 2>/dev/null | head -1)
    if [ -n "$CERT" ] && [ -f "$CERT" ]; then
        EXPIRY=$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)
        DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
        ISSUER=$(openssl x509 -issuer -noout -in "$CERT" | grep -oP 'O=\K[^,/]+')
        if [ $DAYS -lt 30 ]; then
            STATUS="⚠ СКОРО ИСТЕКАЕТ"
        elif [ $DAYS -lt 0 ]; then
            STATUS="❌ ИСТЁК"
        else
            STATUS="✓ OK"
        fi
        printf "%-35s %-20s %3d дней  %-15s %s\n" "$SERVER" "$ISSUER" "$DAYS" "$STATUS" "$CERT"
    fi
done

# Результат:
# mebel-store.ru               Let's Encrypt   12 дней  ⚠ СКОРО ИСТЕКАЕТ  /etc/letsencrypt/live/mebel-store.ru/fullchain.pem
# stroydom-kazan.ru            Comodo           340 дней ✓ OK              /etc/ssl/certs/stroydom.pem
# api.mobile-app.ru            Let's Encrypt   -3 дней  ❌ ИСТЁК           /etc/letsencrypt/live/api.mobile-app.ru/fullchain.pem
# ...

Инвентаризация показала: 32 сертификата Let's Encrypt (из них 5 истекают в ближайшие 2 недели), 8 коммерческих (Comodo/DigiCert) и 5 самоподписанных (внутренние сервисы). Автообновление certbot было настроено, но cron-задача не запускалась из-за ошибки прав доступа.

Корневые причины проблем

Мы выявили четыре причины, из-за которых сертификаты истекали:

  • Сломанный cron certbot/etc/cron.d/certbot запускал обновление от имени пользователя www-data, у которого не было прав на перезапуск nginx
  • HTTP challenge невозможен для некоторых доменов — 6 доменов были за Cloudflare CDN, и HTTP-01 challenge не проходил
  • Коммерческие сертификаты — обновлялись только вручную, сроки терялись
  • Нет мониторинга — никто не получал уведомления о приближающемся истечении

Настройка certbot с автоматическим обновлением

Мы перевели все 45 сайтов на Let's Encrypt и настроили надёжное автообновление.

Установка certbot и плагинов

# Установка certbot с плагинами для nginx и Cloudflare DNS
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx python3-certbot-dns-cloudflare

# Проверка версии
certbot --version
# certbot 2.9.0

# Для Apache-сервера (третий сервер клиента)
sudo apt-get install -y python3-certbot-apache

Получение сертификатов через HTTP-01 challenge

Для доменов, не использующих Cloudflare CDN, мы настроили стандартный HTTP-01 challenge:

# Получение сертификата для одного домена
sudo certbot --nginx -d mebel-store.ru -d www.mebel-store.ru \
  --non-interactive --agree-tos --email admin@webcraft.ru \
  --redirect --staple-ocsp

# Массовое получение сертификатов
# Список доменов (по одному на строку, домен + www)
cat > /tmp/domains.txt <<'EOF'
mebel-store.ru,www.mebel-store.ru
kazan-flowers.ru,www.kazan-flowers.ru
auto-parts-kzn.ru,www.auto-parts-kzn.ru
beauty-salon-kazan.ru,www.beauty-salon-kazan.ru
restoran-kazanochka.ru,www.restoran-kazanochka.ru
EOF

# Скрипт массового получения
while IFS= read -r domains; do
    DOMAIN_ARGS=$(echo "$domains" | tr ',' ' ' | sed 's/[^ ]* */-d &/g')
    echo "Получаем сертификат для: $domains"
    sudo certbot --nginx $DOMAIN_ARGS \
      --non-interactive --agree-tos --email admin@webcraft.ru \
      --redirect --staple-ocsp \
      --keep-until-expiring
    echo "---"
done < /tmp/domains.txt

Wildcard-сертификаты через Cloudflare DNS challenge

Для 6 доменов за Cloudflare CDN HTTP challenge невозможен — DNS-запись A указывает на Cloudflare proxy, а не на наш сервер. Решение — DNS-01 challenge через Cloudflare API:

# Создаём файл с API-токеном Cloudflare
sudo mkdir -p /etc/letsencrypt/cloudflare
sudo tee /etc/letsencrypt/cloudflare/credentials.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini

# Получаем wildcard-сертификат
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d "stroydom-kazan.ru" \
  -d "*.stroydom-kazan.ru" \
  --non-interactive --agree-tos --email admin@webcraft.ru

# Проверяем полученный сертификат
sudo openssl x509 -in /etc/letsencrypt/live/stroydom-kazan.ru/fullchain.pem \
  -noout -subject -dates -ext subjectAltName
# subject= CN = stroydom-kazan.ru
# notBefore=Mar 31 00:00:00 2026 GMT
# notAfter=Jun 29 00:00:00 2026 GMT
# X509v3 Subject Alternative Name:
#     DNS:stroydom-kazan.ru, DNS:*.stroydom-kazan.ru

Wildcard-сертификат покрывает все поддомены: www, api, admin, cdn — один сертификат вместо четырёх.

Настройка автообновления и reload hooks

Критически важная часть — certbot должен не только обновить сертификат, но и перезагрузить веб-сервер:

# Создаём deploy hooks для nginx и Apache
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy

# Hook для nginx
sudo tee /etc/letsencrypt/renewal-hooks/deploy/01-reload-nginx.sh > /dev/null <<'HOOK'
#!/bin/bash
# Перезагружаем nginx после обновления сертификата
if systemctl is-active --quiet nginx; then
    systemctl reload nginx
    logger "certbot: nginx перезагружен после обновления сертификата $RENEWED_DOMAINS"
fi
HOOK
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/01-reload-nginx.sh

# Hook для Apache (на третьем сервере)
sudo tee /etc/letsencrypt/renewal-hooks/deploy/01-reload-apache.sh > /dev/null <<'HOOK'
#!/bin/bash
if systemctl is-active --quiet apache2; then
    systemctl reload apache2
    logger "certbot: apache2 перезагружен после обновления сертификата $RENEWED_DOMAINS"
fi
HOOK
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/01-reload-apache.sh

# Hook для уведомления в Telegram
sudo tee /etc/letsencrypt/renewal-hooks/deploy/99-notify.sh > /dev/null <<'HOOK'
#!/bin/bash
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"
MSG="SSL обновлён: $RENEWED_DOMAINS на $(hostname) ($(date '+%Y-%m-%d %H:%M'))"
curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -d chat_id="${CHAT_ID}" -d text="${MSG}" > /dev/null
HOOK
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/99-notify.sh

# Настраиваем systemd timer вместо cron (надёжнее)
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

# Проверяем расписание
systemctl list-timers certbot.timer
# NEXT                         LEFT          LAST   PASSED  UNIT          ACTIVATES
# Tue 2026-04-01 03:25:00 MSK  23h left      Mon... 1h ago  certbot.timer certbot.service

# Тестовый запуск (dry run)
sudo certbot renew --dry-run
# Processing /etc/letsencrypt/renewal/mebel-store.ru.conf
# Simulating renewal of an existing certificate for mebel-store.ru and www.mebel-store.ru
# ...
# Congratulations, all simulated renewals succeeded:
#   /etc/letsencrypt/live/mebel-store.ru/fullchain.pem (success)
#   /etc/letsencrypt/live/stroydom-kazan.ru/fullchain.pem (success)
#   ... (45 сертификатов)

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

Автообновление — это хорошо, но что если certbot не сможет обновить сертификат (например, DNS недоступен или API Cloudflare вернул ошибку)? Нужен независимый мониторинг, который заблаговременно предупредит об истечении.

Скрипт проверки сроков SSL

Мы написали скрипт, который проверяет все сертификаты снаружи (через реальное TLS-подключение) и изнутри (файлы на диске):

#!/bin/bash
# /opt/scripts/check-ssl-expiry.sh
# Проверяет SSL-сертификаты всех доменов и алертит при приближении срока

DOMAINS=(
    "mebel-store.ru"
    "stroydom-kazan.ru"
    "kazan-flowers.ru"
    "auto-parts-kzn.ru"
    "beauty-salon-kazan.ru"
    "restoran-kazanochka.ru"
    # ... все 45 доменов
)

WARN_DAYS=21    # Предупреждение за 21 день
CRIT_DAYS=7     # Критический алерт за 7 дней
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"
LOGFILE="/var/log/ssl-check.log"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] SSL check started" >> "$LOGFILE"

for domain in "${DOMAINS[@]}"; do
    # Получаем дату истечения через TLS-подключение
    EXPIRY=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null \
             | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

    if [ -z "$EXPIRY" ]; then
        MSG="SSL CHECK FAILED: $domain — не удалось подключиться"
        echo "[$(date)] $MSG" >> "$LOGFILE"
        curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
          -d chat_id="${CHAT_ID}" -d text="$MSG" > /dev/null
        continue
    fi

    EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
    NOW_EPOCH=$(date +%s)
    DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

    if [ $DAYS_LEFT -lt 0 ]; then
        MSG="SSL EXPIRED: $domain истёк $((DAYS_LEFT * -1)) дней назад!"
        curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
          -d chat_id="${CHAT_ID}" -d text="$MSG" > /dev/null
    elif [ $DAYS_LEFT -lt $CRIT_DAYS ]; then
        MSG="SSL CRITICAL: $domain истекает через $DAYS_LEFT дней ($EXPIRY)"
        curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
          -d chat_id="${CHAT_ID}" -d text="$MSG" > /dev/null
    elif [ $DAYS_LEFT -lt $WARN_DAYS ]; then
        MSG="SSL WARNING: $domain истекает через $DAYS_LEFT дней ($EXPIRY)"
        curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
          -d chat_id="${CHAT_ID}" -d text="$MSG" > /dev/null
    fi

    echo "[$(date)] $domain: $DAYS_LEFT дней" >> "$LOGFILE"
done

echo "[$(date '+%Y-%m-%d %H:%M:%S')] SSL check completed" >> "$LOGFILE"
# Добавляем в cron (ежедневная проверка в 9:00)
echo '0 9 * * * root /opt/scripts/check-ssl-expiry.sh' | sudo tee /etc/cron.d/ssl-check
sudo chmod 644 /etc/cron.d/ssl-check

Мониторинг в Prometheus и Grafana

Для более продвинутого мониторинга мы развернули blackbox_exporter — компонент Prometheus для проверки внешних эндпоинтов:

# Установка blackbox_exporter
wget https://github.com/prometheus/blackbox_exporter/releases/download/v0.25.0/blackbox_exporter-0.25.0.linux-amd64.tar.gz
tar xvf blackbox_exporter-0.25.0.linux-amd64.tar.gz
sudo mv blackbox_exporter-0.25.0.linux-amd64/blackbox_exporter /usr/local/bin/

# Конфигурация blackbox_exporter
sudo tee /etc/blackbox_exporter/config.yml > /dev/null <<'EOF'
modules:
  http_2xx_ssl:
    prober: http
    timeout: 10s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: [200, 301, 302]
      method: GET
      tls_config:
        insecure_skip_verify: false
      fail_if_ssl: false
      fail_if_not_ssl: true
EOF

# Systemd unit
sudo tee /etc/systemd/system/blackbox-exporter.service > /dev/null <<'EOF'
[Unit]
Description=Blackbox Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/blackbox_exporter --config.file=/etc/blackbox_exporter/config.yml
Restart=always
User=nobody

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now blackbox-exporter

Конфигурация Prometheus для сбора SSL-метрик:

# Добавляем в /etc/prometheus/prometheus.yml

  - job_name: 'ssl-monitoring'
    metrics_path: /probe
    params:
      module: [http_2xx_ssl]
    static_configs:
      - targets:
          - https://mebel-store.ru
          - https://stroydom-kazan.ru
          - https://kazan-flowers.ru
          - https://auto-parts-kzn.ru
          - https://beauty-salon-kazan.ru
          # ... все 45 доменов
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9115  # blackbox_exporter
# Алерты для SSL в Prometheus
# /etc/prometheus/rules/ssl_alerts.yml
groups:
  - name: ssl
    rules:
      - alert: SSLCertExpiringSoon
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 21
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "SSL сертификат {{ $labels.instance }} истекает менее чем через 21 день"
          description: "Осталось {{ $value | humanizeDuration }}"

      - alert: SSLCertExpireCritical
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "SSL сертификат {{ $labels.instance }} истекает менее чем через 7 дней!"

      - alert: SSLCertExpired
        expr: probe_ssl_earliest_cert_expiry - time() < 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "SSL сертификат {{ $labels.instance }} ИСТЁК!"

      - alert: SSLProbeFailure
        expr: probe_success{job="ssl-monitoring"} == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Не удалось подключиться к {{ $labels.instance }} по HTTPS"

Cert-manager для Kubernetes-сайтов

8 сайтов клиента работали в Kubernetes-кластере. Для них мы развернули cert-manager — контроллер, который автоматически получает и обновляет сертификаты внутри кластера.

Установка cert-manager через Helm

# Установка cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --version v1.14.0

# Проверяем установку
kubectl -n cert-manager get pods
# NAME                                      READY   STATUS    RESTARTS
# cert-manager-7d8f6b5c4-x2k9l             1/1     Running   0
# cert-manager-cainjector-5b8f6b5c4-m4n7p  1/1     Running   0
# cert-manager-webhook-6b8f6b5c4-p9r2s     1/1     Running   0

ClusterIssuer для Let's Encrypt и Cloudflare DNS

Создаём два ClusterIssuer — для staging (тестирование) и production:

# Секрет с API-токеном Cloudflare
kubectl -n cert-manager create secret generic cloudflare-api-token \
  --from-literal=api-token=YOUR_CLOUDFLARE_API_TOKEN

# ClusterIssuer для production
cat <<'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@webcraft.ru
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      # HTTP-01 challenge для обычных доменов
      - http01:
          ingress:
            class: nginx
      # DNS-01 challenge для wildcard (Cloudflare)
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token
        selector:
          dnsZones:
            - "stroydom-kazan.ru"
            - "webcraft-agency.ru"
EOF

# Проверяем статус
kubectl get clusterissuer
# NAME               READY   AGE
# letsencrypt-prod   True    30s

Теперь достаточно добавить аннотацию к Ingress-ресурсу, и cert-manager автоматически получит сертификат:

# Пример Ingress с автоматическим SSL
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mebel-store-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts:
        - mebel-store.ru
        - www.mebel-store.ru
      secretName: mebel-store-tls
  rules:
    - host: mebel-store.ru
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mebel-store-svc
                port:
                  number: 80

# Проверяем статус сертификата
kubectl get certificate
# NAME              READY   SECRET            AGE
# mebel-store-tls   True    mebel-store-tls   2m

kubectl describe certificate mebel-store-tls
# Events:
#   Normal  Issuing    2m    cert-manager  Issuing certificate
#   Normal  Generated  2m    cert-manager  Generated new private key
#   Normal  Requested  2m    cert-manager  Created CertificateRequest
#   Normal  Issuing    90s   cert-manager  The certificate has been issued successfully

Замена коммерческих сертификатов на Let's Encrypt

8 коммерческих сертификатов стоили клиенту около 40 000 руб/год. Мы перевели их на Let's Encrypt.

Процесс замены коммерческого сертификата

Для каждого домена с коммерческим сертификатом:

# 1. Получаем новый сертификат Let's Encrypt
sudo certbot certonly --nginx \
  -d stroydom-kazan.ru -d www.stroydom-kazan.ru \
  --non-interactive --agree-tos --email admin@webcraft.ru

# 2. Обновляем конфигурацию nginx
# Было:
# ssl_certificate /etc/ssl/certs/stroydom-kazan.ru.crt;
# ssl_certificate_key /etc/ssl/private/stroydom-kazan.ru.key;

# Стало:
# ssl_certificate /etc/letsencrypt/live/stroydom-kazan.ru/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/stroydom-kazan.ru/privkey.pem;

sudo sed -i 's|ssl_certificate /etc/ssl/certs/stroydom-kazan.ru.crt;|ssl_certificate /etc/letsencrypt/live/stroydom-kazan.ru/fullchain.pem;|' /etc/nginx/sites-enabled/stroydom-kazan.conf
sudo sed -i 's|ssl_certificate_key /etc/ssl/private/stroydom-kazan.ru.key;|ssl_certificate_key /etc/letsencrypt/live/stroydom-kazan.ru/privkey.pem;|' /etc/nginx/sites-enabled/stroydom-kazan.conf

# 3. Проверяем конфигурацию и перезагружаем
sudo nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

# 4. Проверяем новый сертификат
echo | openssl s_client -connect stroydom-kazan.ru:443 -servername stroydom-kazan.ru 2>/dev/null | openssl x509 -noout -issuer -dates
# issuer= /C=US/O=Let's Encrypt/CN=E6
# notBefore=Mar 31 00:00:00 2026 GMT
# notAfter=Jun 29 00:00:00 2026 GMT

OCSP Stapling и оптимизация SSL

Мы дополнительно оптимизировали SSL-конфигурацию nginx для всех сайтов:

# /etc/nginx/snippets/ssl-params.conf — общие параметры SSL

# Протоколы и шифры
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# SSL session cache
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# OCSP Stapling (ускоряет TLS handshake)
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;

# DH params (для TLS 1.2)
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
# Генерация DH-параметров (один раз)
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# Подключаем в каждый server-блок
# server {
#     listen 443 ssl http2;
#     include snippets/ssl-params.conf;
#     ssl_certificate /etc/letsencrypt/live/domain.ru/fullchain.pem;
#     ssl_certificate_key /etc/letsencrypt/live/domain.ru/privkey.pem;
#     ...
# }

# Проверяем OCSP stapling
openssl s_client -connect mebel-store.ru:443 -status /dev/null | grep -A 3 'OCSP Response'
# OCSP Response Status: successful (0x0)
# OCSP Response Data:
#     Response Status: good

Дашборд SSL-мониторинга в Grafana

Мы создали единый дашборд в Grafana, где все 45 сайтов видны на одном экране.

PromQL-запросы для дашборда

Основные панели дашборда:

# Панель 1: Дней до истечения (таблица)
# PromQL:
(probe_ssl_earliest_cert_expiry - time()) / 86400

# Панель 2: Сертификаты, истекающие в ближайшие 30 дней (список)
# PromQL:
(probe_ssl_earliest_cert_expiry - time()) / 86400 < 30

# Панель 3: Статус HTTPS-проверки (StatusMap)
# PromQL:
probe_success{job="ssl-monitoring"}

# Панель 4: Версия TLS
# PromQL:
probe_tls_version_info{job="ssl-monitoring"}

# Панель 5: Время TLS handshake
# PromQL:
probe_http_duration_seconds{phase="tls",job="ssl-monitoring"}

Дашборд позволял в одном месте видеть статус всех 45 сертификатов: зелёный — более 30 дней, жёлтый — менее 21 дня, красный — менее 7 дней. Технический директор WebCraft открывал его каждое утро.

Ежедневный отчёт в Telegram

Дополнительно к алертам мы настроили ежедневный сводный отчёт:

#!/bin/bash
# /opt/scripts/ssl-daily-report.sh
# Ежедневный отчёт по SSL-сертификатам

DOMAINS_FILE="/opt/scripts/domains.list"
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"

TOTAL=0
OK=0
WARN=0
CRIT=0
EXPIRED=0
DETAILS=""

while IFS= read -r domain; do
    [ -z "$domain" ] && continue
    TOTAL=$((TOTAL + 1))

    EXPIRY_EPOCH=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null \
      | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 | xargs -I{} date -d {} +%s 2>/dev/null)

    if [ -z "$EXPIRY_EPOCH" ]; then
        CRIT=$((CRIT + 1))
        DETAILS="${DETAILS}\n  $domain — НЕДОСТУПЕН"
        continue
    fi

    DAYS=$(( (EXPIRY_EPOCH - $(date +%s)) / 86400 ))

    if [ $DAYS -lt 0 ]; then
        EXPIRED=$((EXPIRED + 1))
        DETAILS="${DETAILS}\n  $domain — ИСТЁК ($DAYS дн.)"
    elif [ $DAYS -lt 7 ]; then
        CRIT=$((CRIT + 1))
        DETAILS="${DETAILS}\n  $domain — $DAYS дн."
    elif [ $DAYS -lt 21 ]; then
        WARN=$((WARN + 1))
        DETAILS="${DETAILS}\n  $domain — $DAYS дн."
    else
        OK=$((OK + 1))
    fi
done < "$DOMAINS_FILE"

MSG="SSL-отчёт $(date '+%d.%m.%Y'):
  Всего: $TOTAL
  OK (>21д): $OK
  Warning (<21д): $WARN
  Critical (<7д): $CRIT
  Истекших: $EXPIRED"

if [ $WARN -gt 0 ] || [ $CRIT -gt 0 ] || [ $EXPIRED -gt 0 ]; then
    MSG="${MSG}\n\nТребуют внимания:${DETAILS}"
fi

curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -d chat_id="${CHAT_ID}" \
  -d text="$(echo -e "$MSG")" > /dev/null
# Cron: ежедневно в 9:00
echo '0 9 * * * root /opt/scripts/ssl-daily-report.sh' | sudo tee /etc/cron.d/ssl-report

Результаты внедрения

Проект занял 4 рабочих дня. Вот измеримые результаты автоматизации SSL специалистами АйТи Фреш для агентства WebCraft:

МетрикаДо автоматизацииПосле автоматизации
Просроченных сертификатов за квартал3 инцидента0 инцидентов
Время на управление сертификатами~4 часа/месяц (ручное)0 (полная автоматизация)
Расходы на коммерческие SSL~40 000 руб/год0 руб (Let's Encrypt)
Время реакции на проблему1–4 часа (утренний звонок клиента)5 минут (алерт в Telegram)
Покрытие мониторингом0%100% (45 доменов + 8 в K8s)
OCSP StaplingНе настроен100% сайтов

За три месяца после внедрения certbot автоматически обновил 135 сертификатов (45 доменов × 3 обновления за 90-дневный цикл). Ни одного инцидента. Мониторинг дважды обнаружил проблемы до их наступления: один раз DNS-провайдер был недоступен при DNS challenge, второй раз — Cloudflare API вернул 429 (rate limit). Оба раза алерт пришёл за 14 дней до истечения, что дало достаточно времени для реагирования.

«Раньше SSL-сертификаты были постоянной головной болью. Сейчас я вообще о них не думаю — получаю отчёт в Telegram каждое утро, и там всегда зелёный статус. 40 000 рублей экономии в год — приятный бонус» — технический директор WebCraft.

Часто задаваемые вопросы

Технически — ничем. Let's Encrypt выпускает стандартные DV (Domain Validation) сертификаты, которые поддерживаются всеми браузерами. Единственные отличия: 1) Срок действия 90 дней (у коммерческих — 1 год), что компенсируется автообновлением; 2) Нет OV/EV-валидации (верификация организации); 3) Нет финансовой гарантии. Для 95% сайтов Let's Encrypt — оптимальный выбор. OV/EV сертификаты нужны только банкам и крупным корпорациям.

DNS-01 challenge — метод подтверждения владения доменом, при котором certbot создаёт TXT-запись в DNS зоне домена. Нужен в двух случаях: 1) Для wildcard-сертификатов (*.domain.ru) — HTTP-01 не поддерживает wildcard; 2) Когда сервер недоступен извне (за CDN, firewall). DNS challenge требует API-доступ к DNS-провайдеру. Certbot поддерживает Cloudflare, Route53, Google DNS, DigitalOcean и десятки других через плагины.

Из командной строки: echo | openssl s_client -servername domain.ru -connect domain.ru:443 2>/dev/null | openssl x509 -noout -dates. Результат покажет notBefore и notAfter. Для проверки файла сертификата: openssl x509 -enddate -noout -in /path/to/cert.pem. В браузере: кликните на замок в адресной строке, затем «Сертификат» — «Срок действия».

Да, wildcard-сертификат *.domain.ru покрывает все поддомены первого уровня: www, api, admin, mail и т.д. Но он НЕ покрывает сам домен domain.ru (без поддомена) и поддомены второго уровня (sub.api.domain.ru). Поэтому при получении wildcard мы всегда указываем оба варианта: -d domain.ru -d *.domain.ru.

Типичные причины и решения: 1) Порт 80 закрыт — HTTP-01 challenge требует доступ к порту 80, проверьте firewall; 2) DNS не обновляется — при DNS challenge проверьте API-токен и права; 3) Rate limit Let's Encrypt — не более 50 сертификатов на домен в неделю; 4) Нет прав на перезапуск nginx — deploy hook должен запускаться от root. Команда certbot renew --dry-run покажет ошибку без реального обновления.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#SSL сертификат автоматизация#Let's Encrypt certbot#wildcard сертификат DNS challenge#мониторинг SSL сертификатов#Zabbix проверка сертификатов#cert-manager Kubernetes#Cloudflare API certbot#автообновление сертификатов