Обновляем корпоративный шаблон TLS: ECDHE, HSTS, OCSP stapling в одном конфиге
После FREAK, POODLE и Logjam собрали один nginx-шаблон для всех хостов: PFS через ECDHE, отключение SSLv3/TLS1.0, HSTS, OCSP stapling. Разбираем каждый блок.
После FREAK, POODLE и Logjam 2014-2015 формируются консолидированные рекомендации по TLS/SSL на nginx
За последние полтора года мы разбирали POODLE, FREAK и Logjam по отдельности - каждый раз как реакцию на конкретный CVE. Каждый раз лезли в конфиги, что-то подкручивали, уходили. В итоге накопилась ситуация, которая выглядит примерно так: на одном сервере SSLv3 отключён, на другом - нет, HSTS стоит у троих из восьми, OCSP stapling вообще нигде. Разнобой.
Мы решили сделать то, что надо было сделать сразу: один шаблонный конфиг nginx для TLS-терминации, который раскатывается на все хосты через управляемую инфраструктуру и не оставляет места для «а вот тут мы не успели».
Почему сейчас
Три атаки подряд за 12 месяцев - это не совпадение, это сигнал, что TLS-конфиг нельзя трогать раз в три года. После Logjam Mozilla обновила свои рекомендации, Qualys перестал давать A+ серверам с SSLv3 и 1024-битным DH. Наши клиентские серверы в среднем получали B - не катастрофа, но и не то, что должно стоять на корпоративном портале или личном кабинете.
Плюс подтолкнуло одно конкретное наблюдение: в ходе Logjam-прохода мы выяснили, что часть серверов вообще не терминирует SSL на nginx - трафик идёт через HAProxy или stunnel, и там своя история. Это тоже надо разгребать, но отдельно. Здесь говорим только про nginx как точку TLS-терминации - самый частый случай.
Шаблон
Делаем отдельный файл /etc/nginx/tls-params.conf, который подключается через include в каждый server-блок.
# /etc/nginx/tls-params.conf
# Корпоративный шаблон TLS. Обновлён 2015-06.
# Проверять совместимость при обновлении OpenSSL и nginx.
ssl_protocols TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
ssl_dhparam /etc/ssl/dhparam2048.pem;
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
В server-блоке остаётся только:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
include tls-params.conf;
# ... остальное
}
Что и почему
SSLv3 и TLS 1.0 отключены. SSLv3 мёртв после POODLE - здесь вопросов нет. TLS 1.0 сложнее: формально уязвимость BEAST затрагивает его в режиме CBC, и современные браузеры давно применяют mitigation на стороне клиента. Но для внешних ресурсов мы решили его всё равно убрать - если клиент не поддерживает TLS 1.1, это уже что-то очень старое. Для внутренних систем с широким спектром клиентов TLS 1.0 можно вернуть отдельным конфигом.
ECDHE на первом месте. Perfect Forward Secrecy через ECDHE - это то, ради чего вся история. Даже если сертификат утечёт завтра, прошлые сессии не расшифровываются. ssl_prefer_server_ciphers on гарантирует, что сервер выбирает из топа нашего списка, а не из предпочтений клиента.
ssl_dhparam. Без явного файла DH-параметров nginx использует встроенные 1024-битные группы - уязвимость, которую мы закрывали после Logjam. Файл генерируется один раз: openssl dhparam -out /etc/ssl/dhparam2048.pem 2048.
ssl_ecdh_curve secp384r1. По умолчанию nginx использует prime256v1. Мы выбрали secp384r1 - он шире поддерживается и считается разумным выбором для 2015 года. Здесь можно поспорить, но хуже не будет.
OCSP stapling. Без него браузер при каждом соединении ходит к CA проверять статус сертификата - медленно и добавляет зависимость от внешнего сервиса. Со stapling nginx сам периодически запрашивает OCSP-ответ и прикладывает его к TLS-хендшейку. ssl_stapling_verify on заставляет nginx проверять подпись OCSP-ответа - без этой строки stapling работает, но не проверяет, что ответ настоящий.
HSTS. max-age=31536072 - год. includeSubDomains распространяет политику на поддомены. Здесь надо думать перед включением: если на каком-то поддомене нет HTTPS или сертификат невалидный - браузер заблокирует его на год. Прежде чем включать includeSubDomains, нужно убедиться, что все поддомены покрыты. Мы включили везде, где это проверили.
X-Frame-Options и X-Content-Type-Options. Строго говоря, не TLS, но раз уж делаем шаблон для security-заголовков - заодно.
Совместимость
Отказ от SSLv3 и TLS 1.0 режет IE 6 на XP и очень старые мобильные браузеры. Для среднестатистического корпоративного портала это некритично - но надо проверять. Qualys SSL Labs показывает матрицу совместимости с конкретными браузерами и клиентами. Прогнали по конфигу - убедились, что IE 8 на Win7 заходит, Android 2.3 тоже (через TLS 1.0 - но у нас его нет, значит не зайдёт). Если Android 2.3 критичен - TLS 1.0 надо вернуть.
Как раскатываем
Шаблон живёт в Ansible-роли nginx, файл tls-params.conf раскладывается на все хосты с тегом tls. Сначала прогоняем на staging, проверяем testssl.sh --protocols --ciphers --headers staging.example.com, потом production. После каждого хоста - проверка в Qualys SSL Labs. Целевая оценка - A+.
Что осталось за кадром
Сертификаты Let's Encrypt анонсированы, но публичного доступа ещё нет - на продакшне используем коммерческие, проще с поддержкой и цепочкой промежуточных сертификатов. HSTS Preloading (отправка домена в браузерный список) не делали - это необратимая операция, торопиться незачем. Certificate Transparency - следим, но руками не управляем.
По итогам прохода получили A+ на всех внешних хостах клиентов, где терминация на nginx. Шаблон будем обновлять при изменении рекомендаций - теперь это не «посмотрим когда выйдет следующий CVE», а плановое обслуживание.