Перевод Nginx на HTTPS.

В данной статье я буду продолжать рассказ о переводе своего сайта на использование HTTPS-протокола с поддержкой SSL-шифрования. В предыдущей статье я рассказал о способах получения SSL-сертификатов, а в этой речь пойдет о переводе Nginx под HTTPS. Заодно прикрутим поддержку протокола HTTP/2.

Конфигурация Nginx

Поскольку HTTPS использует 443 TCP порт, то его нужно указать в конфиге. Помимо порта нужно добавить параметр "ssl", указывающий что все соединения на данном порту должны работать в SSL-режиме.

Обычно параметр "ssl" указывают когда сервер работает в двух режимах HTTP и HTTPS одновременно. Раньше в Nginx SSL-режим можно было включать только для всего сервера, а настроить выборочно было нельзя. Поэтому в версии 0.7.14 был добавлен параметр "ssl" для директивы listen, который позволил сделать единый сервер для HTTP/HTTPS сайтов. Но поскольку я хочу прикрутить еще и поддержку HTTP/2-соединений, то параметр ssl должен быть указан.

Было так:
server {
 
    # Порт который будет слушать nginx
    listen 80;
{

Стало так:
server {
 
    # Порт который будет слушать nginx
    listen 443 ssl;
{

После изменения порта в конфиге, убедитесь что 443 порт открыт для приема соединений. В Netfilter/Iptables это можно сделать следующим правилом.

iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT

Что касается поддержки HTTP/2 протокола, то тут все просто, надо всего лишь добавить "http2" в значение директивы listen.

# Порт который будет слушать nginx
listen 443 ssl http2;

Ключи и сертификаты

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

Создадим каталог для их хранения и поместим файлы в него.

mkdir /etc/nginx/ssl

Указываем серверу где искать файл сертификата и ключ.

# Сертификаты и ключи
ssl_certificate     /etc/nginx/ssl/blog.techlist.top.crt;
ssl_certificate_key /etc/nginx/ssl/blogtechlist.key;

Продолжительность сессии и тип кэша

Задаем продолжительность сессии ssl_session_timeout, в течении которой можно повторно использовать установленные параметры соединения. Помимо продолжительности зададим тип и размер кэша хранения параметров сессий ssl_session_cache.

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

# SSL - сессия
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

Ключ Диффи — Хеллмана для DHE-шифров

Добавим файл ключа для авторизации по протоколу Диффи - Хеллмана. Протокол позволяет клиенту и серверу использовать общий секретный ключ для обмена данными в рамках открытой сессии. Сам ключ не передается в целом виде по незащищенному каналу. Между сторонами обмена передаются только его отрывки в виде случайных чисел и общие вычисления основанные на этих числах.

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

Обмен ключом происходит только один раз, во время установки соединения. Когда клиент и сервер "договорились" о секретном ключе, то дальнейший обмен будет происходить при помощи SSL-шифрования без лишних подтверждений. Стороны обмениваются зашифрованными данными, шифруя и дешифруя сообщения общим секретным ключом.

Cоздаем ключ Диффи -Хеллмана. Длина ключа будет 2048-bit.

openssl dhparam -out dh_2048.pem 2048

Включаем DH-ключ в конфигурацию.

# Diffie-Hellman ключ для DHE - шифров
ssl_dhparam         /etc/nginx/ssl/dh_2048.pem;

Используемые протоколы

Употребление термина SSL-шифрование является неправильным, его скорее употребляют по привычке, а на деле, в настоящее время все работает на протоколе TLS.

Последняя версия протокола SSL 3.0 вышла в далеком 1996 году, она же послужила основой для создания протокола TLS 1.0. В 2014 году было заявлено о уязвимости протокола SSL 3.0. И с тех пор все потихоньку перебрались на использование TLS протоколов.

Поэтому в значениях директивы ssl_protocols у меня указаны версии протоколов TLS.

# Используемые протоколы
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

Наборы шифров

Директивой ssl_ciphers задаем набор шифров для использования. Про то какие шифры лучше всего использовать можно написать целую статью. Все зависит от того что нужно получить в результате. Шифры влияют на множество параметров, это производительность, совместимость с различными видами браузеров, стойкость, наличие уязвимостей и т.д.

Я сделал проще и воспользовался онлайн генератором от Mozilla. Обратите внимание что для поддержки шифров CHACHA20, Nginx должен быть собран с поддерживающей данный шифр версией Openssl.

# Наборы шифров
 ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';

Директивой ssl_prefer_server_ciphers задаем приоритет серверных шифров. Тем самым указывая клиентам что нужно использовать шифры сервера.

# Приоритет серверных шифров
ssl_prefer_server_ciphers on;

HSTS - HTTP Strict Transport Security

HSTS (HTTP Strict Transport Security) — это механизм защиты от даунгрейд-атак на TLS, он указывает браузеру что для данного сайта всегда нужно использовать TLS. Подобное указание следует из HTTP-заголовка Strict-Transport-Security когда клиент первый раз посещает сайт и имеет определенный срок действия.

# Включение HSTS (15768000 seconds = 6 месяцев)
add_header Strict-Transport-Security max-age=15768000;

Прикрепление OCSP-ответов

Разрешим серверу прикреплять OCSP-ответы (Online Certificate Status Protocol - Протокол проверки статуса SSL-сертификата). Обычно подтверждение сертификата происходит следующим образом. Браузер получает сертификат от сайта и просматривает его серийный номер, после чего обращается к серверам-ответчикам УЦ (Удостоверяющий Центр) чтобы проверить действительность сертификата по полученному серийнику. Иногда браузер может и не получить ответ, потому что ответчики некоторых УЦ работают ненадежно.

Включенный OCSP Stapling решает эту проблему. Ответ OCSP уже содержит электронную подпись, браузеру ведь все равно как она получена, главное чтобы она была действительна. Подпись передается во время установки соединения и действует некоторое время после установки. А сам веб-сервер постоянно связывается с УЦ и обновляет сведения о сертификате, которые потом отдает своим клиентам.

Почти все современные браузеры поддерживают OCSP, кроме CHROME, в нем проверку убрали. Так что смотрите сами, нужна вам эта опция или нет, говорят что при включенных ответах страницы быстрее грузятся, но как мне кажется, человек разницы все равно не увидит.

Прикрепление OCSP-ответов включается директивой ssl_stapling. Чтобы OCSP-ответы работали, дополнительно указывают DNS-сервер директивой resolver. В resolver'е нужно указать ip-адреса DNS-серверов, например публичные DNS Google - 8.8.8.8 и 8.8.4.4, адреса указываются через пробел, не обязательно указывать несколько адресов, можно указать один. Параметр valid является необязательным, он указывает серверу на какой промежуток времени нужно закэшировать полученный ответ.

# OCSP Stapling
ssl_stapling on;
resolver 8.8.8.8 8.8.4.4 valid=300s;

Узнать OCSP-ответ сервера можно следующей командой.

echo QUIT | openssl s_client -connect techlist.top:443 -status 2> /dev/null | grep -A 17 'OCSP response:' | grep -B 17 'Next Update'

Ответом будет подобный вывод, если ничего нет, то OCSP-ответы не поддерживаются.

OCSP response: 
======================================
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response
    Version: 1 (0x0)
    Responder Id: 90AF6A3A945A0BD890EA125673DF43B43A28DAE7
    Produced At: Jun  7 04:43:01 2017 GMT
    Responses:
    Certificate ID:
      Hash Algorithm: sha1
      Issuer Name Hash: 7AE13EE8A0C42A2CB428CBE7A605461940E2A1E9
      Issuer Key Hash: 90AF6A3A945A0BD890EA125673DF43B43A28DAE7
      Serial Number: 6990D80FBE476F572B1A6B871467DF6B
    Cert Status: good
    This Update: Jun  7 04:43:01 2017 GMT
    Next Update: Jun 14 04:43:01 2017 GMT

Редирект на HTTPS

Для клиентов использующих старые ссылки для доступа на ваш сайт, необходимо прописать редирект на HTTPS, чтобы человек пришедший по старому адресу http://test.com был перенаправлен на https://test.com.

# Редирект на HTTPS-протокол
server {
    listen 80;
    server_name test.com www.test.com;
    return 301 https://$server_name$request_uri;
}

Теперь все ломящиеся на 80-й порт будут отправлены на 443-й.

Запрет доступа по ip-адресу при использовании HTTPS

Если нужно запретить доступ по ip-адресу при использовании HTTPS, то можно воспользоваться следующей конструкцией.

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

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

При вводе адреса http://93.170.169.118/ сервер как и положено, разрывал соединение с клиентом, но после перевода сайта на HTTPS нужно было закрыть лазейку и на 443 порту. Потому что при переходе по адресу https://93.170.169.118/ открывался мой сайт, что было не очень хорошо.

Здраво рассудив что вышеуказанная конструкция закрывающая доступ на 80-м порту, должна также работать и для 443-го, я применил ее особо не задумываясь, просто поменяв номер порта.

server {
    listen 443 default_server;
    server_name _;
    return 444;
}

Как оказалось я был прав лишь отчасти. Конструкция работала, при попытке перейти по адресу вида https://93.170.169.118/, сервер разрывал соединение и все было бы хорошо, если бы не одна досадная деталь, сервер разрывал соединение и при попытке перейти по адресу вида https://techlist.top. Говоря другими словами на сайт стало нереально попасть, то есть вообще никак.

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

Делаем самоподписанный сертификат. Сначала ключ.

openssl genrsa -out server.key 2048

Потом CSR-запрос.

openssl req -new -key server.key -out server.csr

Собственно сам сертификат.

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

В итоге мы получили три файла: server.crt, server.csr, server.key, из них нас интересует только два - server.crt и server.key. Скопируем их в каталог /etc/nginx/ssl.

cp server.crt server.key /etc/nginx/ssl

Вот так выглядел промежуточный вариант запрета доступа по ip-адресу через https.

server {
    listen 443 ssl http2 default_server;
    server_name _;
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;
    return 444;
}

При попытке перейти по адресу https://93.170.169.118/, браузер сначала ругался на сертификат, а если продолжить, то сервер разрывал соединение и страница была недоступна. Но обращение по доменному имени работало и я посчитал задачу выполненной.

Но я опять был частично неправ. В дальнейшем, во время онлайн-тестов по проверке SSL, стали вылезать разные косяки, то не работали OCSP-ответы, то не поддерживался кэш сессий. Опытным путем я нашел как устранить эти неполадки, нужно было просто добавить опции отвечающие за кэш сессий и OCSP.

В итоге конечный вариант запрета доступа по ip-адресу через https стал выглядеть так.

server {
    listen 443 ssl http2 default_server;
    server_name _;
		
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;
		
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
		
    ssl_stapling on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
		
    return 444;
}

На самом деле это практически и не нужно. Очень редко кто использует адрес вида https://xx.xx.xx.xx, ведь если вбить ip-адрес в адресную сроку браузера, а потом скопировать его, то можно увидеть что он будет вида http://xx.xx.xx.xx а не https://. Но на всякий случай я решил закрыть эту возможность.

Куда все это писать?

Вся конфигурация отвечающая за SSL, должна находиться в контексте server {} вашего хост-файла.

В одной из предыдущих статей по настройке и конфигурации Nginx, я рассказывал как у меня все организовано. Поэтому мне нужно просто подключить файл с SSL-конфигурацией в контекст server {} своего хост-файла. Например, вот так.

server {
 
    .........................................
	
    # Имя сайта
    server_name test.com www.test.com;
	
    # Корневая директория и индексный файл
    root        /var/www/test/public;
    index       index.php;
	
    # Лог-файлы
    access_log  /var/www/test/logs/access.log;
    error_log   /var/www/test/logs/error.log;

    # Подключение ssl.conf
    include /etc/nginx/conf/ssl.conf;		
    
    .........................................
 
}

А вот и сам файл ssl.conf находящийся в /etc/nginx/conf.

### ssl_techlist.conf ###
### Включается в контекст server {}
### Располагается в /etc/nginx/conf

    # Сертификаты и ключи
    ssl_certificate     /etc/nginx/ssl/blog.techlist.top.crt;
    ssl_certificate_key /etc/nginx/ssl/blogtechlist.key;
    
    # SSL - сессия
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Diffie-Hellman ключ для DHE - шифров
    ssl_dhparam         /etc/nginx/ssl/dh_2048.pem;

    # Используемые протоколы
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	
    # Наборы шифров
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    
    # Приоритет серверных шифров
    ssl_prefer_server_ciphers on;

    # Включение HSTS (Strict-Transport-Security)(15768000 seconds = 6 месяцев)
    add_header Strict-Transport-Security max-age=15768000;
    
    # OCSP Stapling
    ssl_stapling on;
    
    resolver 8.8.8.8 8.8.4.4 valid=300s;

Если вы прописываете все в одном файле, то можно добавить данный конфиг в контекст server {}, сразу после указания корневой директории сайта и лог-файлов.

Например так.

# Запрет доступа по ip-адресу для HTTPS
server {
    listen 443 ssl http2 default_server;
    server_name _;
		
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;
		
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
		
    ssl_stapling on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
		
    return 444;
    }
	
# Редирект на HTTPS-протокол
server {
    listen 80;
    server_name test.com www.test.com;
    return 301 https://$server_name$request_uri;
    }
	
server {

    # Порт который будет слушать nginx
    listen      443 ssl http2;
	
    # Имя сайта
    server_name test.com www.test.com;
	
    # Корневая директория и индексный файл
    root        /var/www/test/public;
    index       index.php;
	
    # Лог-файлы
    access_log  /var/www/test/logs/access.log;
    error_log   /var/www/test/logs/error.log;

    # SSL-секция
    
    # Сертификаты и ключи
    ssl_certificate     /etc/nginx/ssl/blog.techlist.top.crt;
    ssl_certificate_key /etc/nginx/ssl/blogtechlist.key;
    
    # SSL - сессия
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Diffie-Hellman ключ для DHE - шифров
    ssl_dhparam         /etc/nginx/ssl/dh_2048.pem;

    # Используемые протоколы
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	
    # Наборы шифров
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    
    # Приоритет серверных шифров
    ssl_prefer_server_ciphers on;

    # Включение HSTS (Strict-Transport-Security)(15768000 seconds = 6 месяцев)
    add_header Strict-Transport-Security max-age=15768000;
    
    # OCSP Stapling
    ssl_stapling on;
    
    resolver 8.8.8.8 8.8.4.4 valid=300s;      	
    
    Далее указываем все остальные параметры..
    .........................................
 
    }

Результат моего труда и оценку можно посмотреть тут, заодно и проверить свои успехи.

5 КОММЕНТАРИИ

  1. Спасибо! нашел статью так как боролся с проблемой: чужой домен прописал ip моего сервера и выдавал себя за меня. Заблочил все левые домены по вашей статье!!
    Только ngnix при перезагрузке выдавал предупреждение "the size of shared memory zone "SSL" conflicts with already declared size" и я поменял строку ssl_session_cache shared:SSL:50m; на ssl_session_cache shared:SSL:10m; и все завелось!!!

  2. Большое тебе человеческое спасибо! Первый раз в жизни настраиваю [wp-nginx-и прочее] все получается по твоим объяснениям и самое главное написано понятным, логичным языком

Ответить:

Please enter your comment!
Please enter your name here