SSL-сертификаты после Heartbleed: перевыпустить - не значит закрыть
Перевыпуск сертификата без отзыва старого оставляет дыру открытой. Разбираем механику OCSP, настраиваем проверку и мониторинг срока действия.
Волна массового перевыпуска SSL-сертификатов после Heartbleed вскрыла системную проблему: большинство команд перевыпустили сертификат, но не отозвали старый
Прошло около двух недель с Heartbleed, и у нас накопилось наблюдение, которое хочется зафиксировать, пока оно свежее. Почти у каждого второго клиента, где мы делаем аудит инфраструктуры, обнаруживается одна и та же история: сертификат перевыпустили, новый лежит в nginx, но старый - не отозван. Он действует. Браузеры ему доверяют. Ключ к нему, потенциально скомпрометированный через Heartbleed, никуда не делся.
Это не редкий edge case - это массовая картина. Разбираем, почему так происходит и что с этим делать.
Почему отзыв не сделали
Причина простая: процесс перевыпуска сертификата и процесс отзыва старого - это два разных действия в панели CA. Когда в аварийном режиме за один день надо перевыпустить несколько сертификатов на разных хостах - про второй шаг легко забыть. Тем более что CA не подсказывает: выдал новый, ожидает следующей заявки.
Итог: инфраструктура работает на новом сертификате с новым ключом, но старый сертификат со старым (предположительно слитым) ключом продолжает быть валидным для любого, у кого он мог оказаться.
Механика OCSP - что реально происходит при проверке
Есть два стандартных механизма отзыва сертификатов:
- CRL (Certificate Revocation List) - список отозванных сертификатов, который CA публикует по расписанию. Браузер скачивает, проверяет. Проблема - список может обновляться редко и быть большим; браузер кэширует его, и задержка между отзывом и фактическим применением может быть до нескольких часов.
- OCSP Stapling (Online Certificate Status Protocol) - запрос в реальном времени к серверу CA: «этот сертификат действует?». Более актуальный ответ, но добавляет RTT к каждому TLS-хендшейку.
На практике поддержка и того, и другого в браузерах неоднородная. Chrome в некоторых конфигурациях тихо игнорирует ошибки OCSP-запроса (soft fail) - то есть если OCSP-сервер недоступен, браузер разрешает соединение. Это называется fail-open, и это спорное дизайнерское решение с точки зрения безопасности.
OCSP Stapling - правильный способ
Проблему с задержкой и надёжностью OCSP частично решает OCSP Stapling. Идея: сервер сам периодически запрашивает у CA статус своего сертификата и прикладывает подписанный ответ прямо в TLS-хендшейк. Клиент не делает отдельный запрос к CA - получает уже готовый ответ.
Настройка для nginx (1.3.7+):
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/ca-chain.crt;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
Где ca-chain.crt - цепочка сертификатов CA (промежуточный + корневой). Без неё stapling не заработает: nginx должен уметь верифицировать ответ OCSP-сервера.
Проверка, что stapling работает:
openssl s_client -connect example.com:443 -status 2>&1 | grep -A 10 "OCSP response"
Если в выводе OCSP Response Status: successful - stapling активен. Если no response sent - либо не настроен, либо nginx не смог получить ответ от OCSP-сервера CA.
Мониторинг срока действия сертификатов
Отдельная история - мониторинг истечения. Сертификаты, которые перевыпускали в аварийном режиме 7-8 апреля, были выданы на разные сроки - кто на год, кто на два. Дата истечения у них другая, чем у предыдущих. Если мониторинг настроен на старые даты (или не настроен вообще) - сюрприз прилетит в момент истечения без предупреждения.
Простой скрипт для проверки через cron:
#!/bin/bash
DOMAIN="$1"
WARN_DAYS=30
EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -jf "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
echo "WARN: $DOMAIN - сертификат истекает через $DAYS_LEFT дней ($EXPIRY)"
exit 1
fi
echo "OK: $DOMAIN - $DAYS_LEFT дней до истечения"
Для Zabbix или Nagios - есть готовые плагины (check_ssl_cert), которые умеют то же самое с нормальными порогами warning/critical. Важно: пороги ставить не 7 дней, а минимум 30. CA после Heartbleed работают с нагрузкой, ручной перевыпуск может занять несколько дней.
Практика: что проверяем сейчас
При обходе клиентских инфраструктур в рамках постхартблид-аудита у нас три обязательные точки проверки по каждому домену:
Первая - статус отзыва старого сертификата. Смотрим через openssl ocsp или через онлайн-чекеры CA, что сертификат с предыдущим серийным номером действительно отозван. Если нет - инициируем отзыв.
Вторая - OCSP Stapling включён. Если Apache или nginx поддерживают - настраиваем. Без stapling OCSP-проверка зависит от клиента, а клиенты ведут себя по-разному.
Третья - мониторинг даты истечения обновлён. Новые сертификаты - новые даты - проверяем, что алерты привязаны к актуальным значениям.
Что всё это значит в сумме
Heartbleed хорош тем, что сделал видимыми вещи, которые обычно остаются в слепом пятне: механизм отзыва сертификатов, настройка OCSP, мониторинг срока действия. Эти вещи существуют давно, но до аварии с ними никто особо не заморачивался - сертификат выдали, работает, и ладно.
Теперь не ладно. Цепочка «выпустить, установить, отозвать старый, настроить stapling, добавить в мониторинг» стала нормальным рабочим процессом, а не чем-то факультативным. По крайней мере, у тех, кто прошёл апрель осознанно.