Много лет индустрия информационной безопасности старается улучшить стандарты шифрования в сети двумя способами:

  • массовое распространение HTTPS как общего стандарта шифрования для всех сайтов — даже для тех, которым защита формально не требуется. Очень много времени было потрачено на то, чтобы убедить пользователей в важности тотального шифрования абсолютно всех коммуникаций;

  • сокращение сроков выдачи сертификатов SSL/TLS, чтобы стимулировать пользователей внедрять автоматические процедуры/скрипты для автопродления сертификатов, чтобы исключить «человеческий фактор» и забывчивость сисадминов, которые забывают менять сертификаты.

Но иногда этого недостаточно. К сожалению, автоматические скрипты продления сертификатов тоже могут выйти из строя.


Автоматизация

К данному моменту создан богатый арсенал инструментов для автоматизации выдачи и продления сертификатов.

Самые популярные из них используют протокол ACME. Например, win-acme — специализированный ACME-клиент для Windows-систем, или баш-скрипт acme.sh, который отличается широкой поддержкой API разных провайдеров.

win-acme
win-acme

Многие используют бота Certbot от провайдера бесплатных сертификатов Let's Encrypt. У него есть DNS-плагины, чтобы автоматизировать DNS-challenge через API провайдеров (Cloudflare, DuckDNS, Route53 и др.).

Что может пойти не так

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

Последняя история с просроченным сертификатом затронула многих пользователей платформы Bazel.

Bazel — это современная опенсорсная система сборки, разработанная Google для больших и мультиплатформенных проектов. Она обеспечивает быстрые, инкрементальные сборки и надёжное тестирование, что делает её популярной среди крупных компаний.

Можно представить масштаб проблем, если такой инструмент выходит из строя. Именно это произошло 26 декабря 2025 года, когда истёк срок действия SSL-сертификата для bcr.bazel.build и releases.bazel.build, как показано на скриншоте из тикета на Github:

Истёкший сертификат нарушил рабочий процесс сборки всех проектов Bazel. При обращении к сервису пользователи увидели сообщение об ошибке:

ERROR: Error computing the main repository mapping: Error accessing registry https://bcr.bazel.build/: Failed to fetch registry file https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel: PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed

Позже один из разработчиков вкратце объяснил причины инцидента. По его словам, «автоматическое продление заблокировалось из-за добавления новых поддоменов, а уведомления о сбоях продления по какой-то причине не отправлялись». Оказалось, что разработчики Bazel совершенно незнакомы со сферой SSL-сертификатов, им «пришлось суетиться, читать документацию и получать разрешения».

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

Казалось бы, инциденты с просроченными сертификатами давно осталась в прошлом. В прошлые годы все в отрасли сталкивались с этим. Но с повсеместным внедрением автоматического продления сертификатов проблема вроде бы осталась позади. Оказывается, не совсем.

Текущий инцидент с Bazel интересен тем, что из строя вышла именно автоматическая система продления сертификатов, которая «заблокировалась из-за добавления новых поддоменов.

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

Автоматизированные системы усугубляют ситуацию, потому что они работают незаметно — и как будто всё происходит «само собой», если смотреть со стороны. А когда конвейер ломается, то сложно понять суть поломки и возобновить операции.

Судя по всему, в команде Bazel только один или несколько разработчиков обладали компетенциями для решения проблемы, но в момент инцидента они оказались в отпуске или недоступны.

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


В такой ситуации можно порекомендовать внешний мониторинг валидности сертификатов, потому что ACME-клиенты не всегда отправляют корректные уведомления о сбое. ACME-клиент может предполагать, что всё в порядке, потому что получил новый сертификат, в то время как сертификат установлен неправильно (например, служба не перезагружается и продолжает использовать старый сертификат).

Можно использовать скрипт, который запускается каждый день и проверяет сроки действия сертификатов. Примеры таких скриптов см. здесь для разных языков программирования.

Все данные скрипты проверяют срок действия сертификата указанного домена и выводят предупреждение в том случае, если до окончания осталось менее 14-ти дней.

Bash

#!/bin/bash
host="example.com"

end_date=$(echo \
  | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null \
  | openssl x509 -noout -enddate \
  | cut -d= -f2)

# Convert a string like "Jan 15 23:59:59 2026 GMT" to seconds since epoch
end_epoch=$(date -d "$end_date" +%s)
now_epoch=$(date +%s)

days=$(( (end_epoch - now_epoch) / 86400 ))
if [ "$days" -lt 14 ]; then # NOTE: reduce 14-day threshold when certificate lifetimes decrease!
  echo "WARNING! SSL certificate expires in only $days days! Somebody should fix it!"
  # TODO: Add alerting here
else
  echo "ok: certificate valid for $days days"
  # TODO: Add a cronjob "OK" check-in here
fi

Python

#!/usr/bin/env python3
import ssl, socket
from datetime import datetime

host = 'example.com'

with socket.create_connection((host, 443)) as tcp:
    with ssl.create_default_context().wrap_socket(tcp, server_hostname=host) as tls:
        cert = tls.getpeercert()

# Parse a string like "Jan 15 23:59:59 2026 GMT" into a datetime
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')

days = (not_after - datetime.utcnow()).days
if days < 14: # NOTE: reduce 14-day threshold when certificate lifetimes decrease!
    print(f"WARNING! SSL certificate expires in only {days} days! Somebody should fix it!")
    # TODO: Add alerting here
else:
    print(f"ok: certificate valid for {days} days")
    # TODO: Add a cronjob "OK" check-in here

Ruby

#!/usr/bin/env ruby
require "socket"
require "openssl"

host = "example.com"

tcp = TCPSocket.new(host, 443)
tls = OpenSSL::SSL::SSLSocket.new(tcp)
tls.hostname = host
tls.connect
not_after = tls.peer_cert.not_after
tls.sysclose
tcp.close

days = ((not_after - Time.now) / 86400).to_i
if days < 14 # NOTE: reduce 14-day threshold when certificate lifetimes decrease!
  puts "WARNING! SSL certificate expires in only #{days} days! Somebody should fix it!"
  # TODO: Add alerting here
else
  puts "ok: certificate valid for #{days} days"
  # TODO: Add a cronjob "OK" check-in here
end

Node.js (JavaScript)

#!/usr/bin/env node
const tls = require('tls');
const host = 'example.com';
const port = 443;

const socket = tls.connect(port, host, {servername: host}, () => {
    const cert = socket.getPeerCertificate();
    const notAfter = new Date(cert.valid_to);
    const days = Math.floor((notAfter - Date.now()) / (1000*86400));
    socket.end();

    if (days < 14) { // NOTE: reduce 14-day threshold when certificate lifetimes decrease!
        console.log(`WARNING! SSL certificate expires in only ${days} days! Somebody should fix it!`);
        // TODO: Add alerting here
    } else {
        console.log(`ok: certificate valid for ${days} days`);
        // TODO: Add a cronjob "OK" check-in here
    }
});

Go

package main

import (
    "crypto/tls"
    "fmt"
    "time"
)

func main() {
    conn, err := tls.Dial("tcp", "example.com:443", nil)
    if err != nil {
        panic(err)
    }

    notAfter := conn.ConnectionState().PeerCertificates[0].NotAfter
    conn.Close()

    days := int(time.Until(notAfter).Hours() / 24)
    if days < 14 { // NOTE: reduce 14-day threshold when certificate lifetimes decrease!
        fmt.Printf("WARNING! SSL certificate expires in only %d days! Somebody should fix it!\n", days)
        // TODO: Add alerting here
    } else {
        fmt.Printf("ok: certificate valid for %d days\n", days)
        // TODO: Add a cronjob "OK" check-in here
    }
}

Powershell

$hostname = "example.com"
$tcp = New-Object System.Net.Sockets.TcpClient($hostname, 443)
$tls = New-Object System.Net.Security.SslStream($tcp.GetStream())
$tls.AuthenticateAsClient($hostname)

# Upgrade to the newer X509Certificate2 class for its NotAfter property
$cert2  = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate)
$notAfter = $cert2.NotAfter

$tls.Close()
$tcp.Close()

$days = ($notAfter - (Get-Date)).Days
if ($days -lt 14) { # NOTE: reduce 14-day threshold when certificate lifetimes decrease!
    Write-Output "WARNING! SSL certificate expires in only $days days! Somebody should fix it!"
    # TODO: Add alerting here
} else {
    Write-Output "ok: certificate valid for $days days"
    # TODO: Add a cronjob "OK" check-in here
}

Для мониторинга можно порекомендовать ssl_exporter (плагин Prometheus) или любое программное средство, работающее по принципу dead man's switch — предохранитель, срабатывающий при поломке.