Шифруем всё подряд


Эра незашифрованного веба проходит, и это хорошо. В этой инструкции мы предполагаем, что на вашем сервере работает веб-сервер Nginx. И теперь мы сделаем так, чтобы все посетители сайта пользовались исключительно протоколом HTTPS. Кроме этого мы включим HSTS – это «HTTP Strict Transport Security», когда сайт не только поддерживает HTTPS, но и настаивает на его использовании.

Для этого есть множество способов, но я опишу метод под названием «HTTPS termination». Иначе говоря, мы поставим перед веб-сервером обратный прокси, который и будет обеспечивать HTTPS. Это получается проще и гибче, чем настраивать HTTPS только при помощи возможностей веб-сервера. Возможно, вам покажется контринтуитивным, что добавление ещё одного приложения в стек упростит вашу жизнь – но это действительно так.

Уточним, что данный рецепт подходит для серверов на базе Linux, на которых установлен Nginx.

То, что будет работать прежде всех остальных приложений в стопке – это HAProxy. Это в первую очередь приложение для балансировки – он умеет распределять приходящие запросы между разными физическими серверами. Много высоконагруженных сайтов используют его в этом качестве (тот же reddit), но в последней версии у него появилась возможность выполнять SSL termination. Он умеет устанавливать HTTPS-соединения от имени сервера.

Поэтому мы поставим HAProxy, скормим ему наши сертификаты SSL/TLS, поручим перенапрявлять все HTTP запросы на HTTPS, и покажем ему уже сам веб-сервер в качестве бэкенда.

Установка HAProxy


Порты 80 и 443 будут смотреть в интернет и получать HTTP и HTTPS трафик. Все HTTP запросы получат редирект 301 на тот же URL, но по HTTPS, и затем перенаправятся на бэкендовый веб-сервер (nginx) по чистому HTTP.

HAProxy package, включённый в поставку Ubuntu 14.04 LTS довольно старый, поэтому добавим репозиторий:

sudo add-apt-repository ppa:vbernat/haproxy-1.5


Затем обновим исходники и установим приложение:

sudo aptitude update
sudo aptitude install haproxy


Основные настройки лежат в /etc/haproxy/haproxy.cfg. Вот мои настройки:

global
    log /dev/log    local0
    log /dev/log    local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
 
    # Default SSL material locations
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private
 
    ssl-default-bind-ciphers  EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4
    ssl-default-bind-options no-sslv3 no-tlsv10
    tune.ssl.default-dh-param 4096
 
defaults
    log     global
    mode http
    option  httplog
    option  dontlognull
    option  forwardfor
    option  http-server-close
    timeout connect 5000
    timeout client  50000
    timeout server  50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http
 
frontend yourservername
    bind *:80
    bind *:443 ssl crt /etc/ssl/private/cert1.pem crt /etc/ssl/private/cert2.pem
    acl secure dst_port eq 443
    redirect scheme https if !{ ssl_fc }
    rspadd Strict-Transport-Security:\ max-age=31536000;\ includeSubDomains;\ preload
    rsprep ^Set-Cookie:\ (.*) Set-Cookie:\ \1;\ Secure if secure
    default_backend webservername
 
backend webservername
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }
    server webservername 192.168.1.50:80
 
listen stats *:9999
    stats enable
    stats uri /


Кстати, если вам понадобится подсветка синтаксиса конфига HAProxy для vim, её можно взять здесь. Разберём настройки подробнее.

Global


Оставляем первый кусок нетронутым. Это настройки логов, директория и права доступа.

Следующие части нужно поправить – там указано, где находится CA root и лежат сертификаты SSL/TLS. Возможно, нужно будет поменять ca-base и crt-base.

Строка ssl-default-bind-ciphers определяет, какие коды SSL/TLS будут применяться HAProxy при соединении с клиентом. Я использую рекомендованный список от Qualys/SSL Labs. Я также отредактировал строчку ssl-default-bind-options и запретил SSLv3 и TLS1.0, т.к. они дырявые. Последняя строка, tune.ssl.default-dh-param, сообщает программе о необходимости использовать не более 4096 бит в параметре Diffie-Hellman при обмене ключами DHE.

Defaults


Добавляем пару вещей — forwardfor и http-server-close. Поскольку мы используем приложение в качестве прокси, оно должно сообщать серверу IP-адреса, с которых идут запросы. Иначе это будет выглядеть, будто весь трафик идёт с HAProxy. Поэтому forwardfor сообщает, что программа работает как обратный прокси, и необходимо добавлять заголовок X-Forwarded For для сервера.

Настройка http-server-close нужна для быстродействия — HAProxy будет решать, закрывать ли соединение или использовать его повторно, при этом поддерживая более продвинутые вещи вроде WebSockets.

Certificates


Первая часть секции сообщает, какой трафик HAproxy должна обрабатывать и куда его отправлять.

Мы привязываем HAproxy к портам 80 и 443, она слушает HTTP на порту 80 и HTTPS на порту 443. Для HTTP мы скармливаем ей два разных сертификата. HAproxy использует Server Name Identification (SNI) чтобы привести хост входящего запроса в соответствие с нужным SSL/TLS сертификатом. У меня на сервере есть три сайта, они используют разные групповые сертификаты (*.bigdinosaur.org, *.chroniclesofgeorge.com и *.bigsaur.us) и HAProxy правильно выбирает нужный из них.

Единственное, что надо сделать – объединить файлы сертификата и приватного ключа в один .pem:

cat your-decrypted-ssl-key.key your-ssl-cert.crt > your-ssl-cert.pem


Убедитесь, что владельцем файла будет root:root, и права у него должны быть только на чтение:

sudo chown root:root your-ssl-cert.pem
chmod 400 your-ssl-cert.pem


От HTTP к HTTPS, и HSTS в качестве бонуса


Теперь определим ACL, список контроля доступа. В HAProxy это список вещей, удовлетворяющих определённому критерию:

acl secure dst_port eq 443


Мы создали ACL по имени secure, который совпадает со всем, что идёт на TCP порт 443. Он нам скоро понадобится.

Следующая строка – та самая, где происходит перенаправление трафика с HTTP на HTTPS.

redirect scheme https if !{ ssl_fc }


Это значит, что если входящий запрос не HTTPS, то надо отправить перенаправление 301 к тому же ресурсу, но по схеме HTTPS.

Это именно то HTTP Strict Transport Security – всем браузерам предписывается использование HTTPS:

rspadd Strict-Transport-Security:\ max-age=31536000;\ includeSubDomains;\ preload


Настройка добавляет нужную строку в заголовки. Браузер, распознающий этот заголовок, понимает, что сайт предпочитает работать по HTTPS, и это указание действительно в течение года (31,536,000 seconds). Директива preload сообщает Google-боту, что ваш сайт можно добавить в их список сайтов, поддерживающих HSTS.

HSTS – штука правильная. Шифрование должно быть всегда, и администраторам надо стремиться распространять его везде.

Кстати – необходимо учесть, чтобы все куки также включали атрибут secure, поскольку с точки зрения вашего веб-сервера всё происходит по обычному протоколу HTTP. Для этого используем директиву rsprep:
1

rsprep ^Set-Cookie:\ (.*) Set-Cookie:\ \1;\ Secure if secure


Обратите внимание на «if secure». Это значит, что меняются только куки, идущие по HTTPS (secure – это переменная, которую мы несколько линий назад определили). Вообще, всё должно работать через HTTPS, поэтому это в принципе необязательно – но можно и подстраховаться.

Последняя строка определяет, куда отправлять трафик. Это default_backend. Тут можно определять несколько серверов для распределения нагрузки и т.п. Но, поскольку у нас есть один бэкенд-сервер, то всё довольно просто.

back end


Поскольку у нас один бэкенд-сервер, эта секция коротка. HAProxy необходимо лишь добавить пару заголовков, чтобы сервер понял, что реальное общение происходит через HTTPS, даже если он видит только лишь HTTP:

http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }


Первая директива устанавливает заголовок, объясняющий, что клиент изначально пришёл на порт 443. Вторая усиливает первую, устанавливая X-Forwarded-Proto в HTTPS, если запрос шёл через HTTPS, чтобы веб-сервер понимал, что происходит и не создавал неправильных ответов.

Затем мы объясняем HAProxy, куда слать запросы – имя, IP и порт веб-сервера. У меня веб-сервер работал на отдельной машине, поэтому я пишу сюда другой IP и 80-й порт. Если у вас всё работает на одном компьютере, то пишите localhost и порт.

Статистика, если нужно


Последняя секция диктует HAProxy выдавать статусную страницу по заданному порту. Тут можно добавить basic auth, добавив stats auth username:password, или определить отличающийся URL.

Если вы используете директиву HSTS «includesubdomains», у вас может не получиться запросить статусную страницу по имени, поскольку веб-браузер попытается загрузить её HTTPS-версию, а HAProxy отдаёт только HTTP-версию. Это можно обойти, запрашивая страницу по IP вместе с портом (http://192.168.x.x:9999).

Подчищаем


Сохраните настройки, но пока не перезапускайте сервис HAProxy. Если у вас всё работает на одной машине, и nginx также слушает события на портах 80 и 443, нужно кое-что подправить.

Поскольку nginx больше не будет отдавать HTTPS-запросы, нужно убрать все упоминания HTTPS из всех файлов виртуальных хостов.

Вот и всё. Нужно только добавить в главный файл nginx.conf две строчки, чтобы убедиться, что nginx будет заменять ip-адреса согласно заголовку X-Forwarded-For, если запросы приходят от 127.0.0.1:

set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;


Это будет работать, если вы скомпилили nginx с опцией ngx_http_realip_module.

Перезапустите HAProxy (service haproxy restart) и она начнёт слушать запросы.

Проверка работы


Сделайте запрос к сайту через http. Если URL меняется на https и вы видите сайт – всё работает. Можно убедиться, что заголовок HSTS отправляется корректно:

curl -s -D- https://yoursite.whatever/ | grep Strict

Комментарии (37)


  1. farcaller
    10.05.2015 01:07
    +39

    Аналогично для nginx решается через:

    listen 80;
    rewrite ^ https://$server_name$request_uri? permanent;
    

    для HTTP, и
    listen 443 ssl spdy;
    
    ssl on;
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;
    ssl_certificate /etc/nginx/ssl/xxx.pem;
    ssl_certificate_key /etc/nginx/ssl/yyy.key;
    
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    # ssl_protocols TLSv1.1 TLSv1.2; для поддержки старого андроида
    ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:DES-CBC3-SHA:!DES:!RC4:!aNULL:!eNULL:!LOW:!MD5:!EXP:!PSK:!SRP:!DSS:!CAMELLIA:!SEED";
    ssl_prefer_server_ciphers on;
    
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 5m;
    
    ssl_stapling on;
    ssl_stapling_verify off;
    ssl_trusted_certificate /etc/nginx/ssl/zzz.pem;
    resolver 8.8.8.8;
    
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    

    для HTTPS


    1. farcaller
      10.05.2015 01:25

      для поддержки старого андроида
      конечно же имелось в виду — если не нужна поддержка старого андроида


    1. jonic
      10.05.2015 02:16
      +20

      Тот случай когда коммент полезнее топика.


    1. AlexWinner
      10.05.2015 08:46
      +8

      Я бы только заменил

      rewrite ^ https://$server_name$request_uri? permanent;

      на

      return 301 https://$server_name$request_uri;

      ИМХО return логичнее и читабельнее.


      1. shamanis
        11.05.2015 08:59

        А я заменил https://$server_name$request_uri на https://$server_name$uri, т.к. у Nginx есть баг при редиректе дублировать GET-переменные. Сам столкнулся недавно. Но, возможно, данный баг возникает только при стеке с Django.


        1. VBart
          11.05.2015 14:57
          +6

          Баг на самом деле не в nginx, и заключается в том, что люди не читают документацию, где черными русскими буквами на белом фоне написано:

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

          rewrite ^/users/(.*)$ /show?user=$1? last;
          
          Обратите внимание, что у человека выше было указано правильно: https://$server_name$request_uri?, хотя как уже было сказано, правильнее будет использовать return.


    1. Aclz
      10.05.2015 12:22

      Оно чем-то лучше/хуже habrahabr.ru/post/252821?


      1. farcaller
        10.05.2015 13:54

        Почти что идентично, оно основано как раз на этом посте.


    1. armab
      10.05.2015 16:18
      -2

      У меня уже давно правильно сконфигурированный SSL является одним из факторов оценки IT компании (и ее команды).

      К сожалению, очень многие не парятся и в тестах их SSL www.ssllabs.com/ssltest показывает целый букет брешей.
      Наиболее встречаемые: POODLE, использование слабого RC4, использование SHA1 сертификата вместо SHA2.

      Реально эти тесты косвенно показывают:

      - насколько компания реально заботится о безопасности
      - насколько админ/команда технически подкованы
        - следят за новостями безопасности
        - если такие простые элементы безопасности не учтены, то что же творится в самой системе?
        - косвенно: насколько hire отдел/CEO квалифицирован при выборе работников
      



      1. Remper
        10.05.2015 18:23
        +5

        «технически подкованы» и «такие простые элементы безопасности» — очень большое преувеличение. Конечно всегда удобно оценивать других людей с позиции своей предметной области. Например, если я не эксперт в инфобезе, почему я должен знать про POODLE? Разве не для этого мы все не лезем в криптографию и используем надёжные библиотеки, которые стабильно обновляем, чтобы не беспокоиться об очередном баге, который так и так вылезает каждые n месяцев?
        А теперь из этого постулата просто представьте, что не каждой компании по карману содержать отдельного инфобезника.
        Лучше патчи пишите в популярные проекты (типа того же nginxа), чтобы людей предупреждали о SHA1 и SSLv3 ворнингом, чем всех по своей линейке мерять.


        1. armab
          10.05.2015 19:53
          +2

          Вы правы, это сугубо субъективная оценка и наверное планка слишком высока.
          Здесь главное не впадать в крайности. Это можно применять к tech-компаниям с 10+ человек в команде, где уже какие-то тех. процессы устоялись и обычно кто-то занимается серверами. Конечно, бесмысленно говорить об очень маленьких компаниях вроде «2 человека и собака» и конечно есть исключения из правил.

          Приведу пример:
          Западный стартап, с помощью которого можно заказать чартер/частный самолет. $6к+ средняя транзакция. Полный букет брешей в SSL тесте — первый плохой знак.
          Смотрим глубже — PHP 5.3 светится в хедерах. Дальше — domain.com/blog/readme.html — старая дырявая версия Wordpress.
          Можно ли уже сказать что-то о технической части проекта? Спрашиваем овнера: «Что происходит?». Оказывается работают индусы за копейки :)

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

          Просто это не единственный фактор, это часть паттерна, некий звоночек.


          1. Remper
            11.05.2015 19:47

            Дело даже не в высоте планки, скорее в её специализированности. Такие ресурсы как хабр конечно же помогают (наверное все слышали хотя бы про Heartbleed) + у большинства есть какие-то базовые представления независящие от инфобеза — что ввод надо фильтровать, что html надо эскейпить, что библиотеки нужно своевременно обновлять.
            Моя мысль больше в том, что очень легко можно пропустить какой-то абстрактный POODLE и это больше показатель, что комьюнити не достаточно хорошо справляется с задачей оповещения о таких вещах. Опять же — какие-то ворнинги от заведомо опасной конфигурации могли бы быть полезны.
            То есть мы не можем ожидать, что все будут подкованы в инфобезе. И это справедливо для любой области. Например, если вы не спец в базах данных, вы можете плохо спроектировать базу данных, можете писать неэффективные запросы и в большинстве случаев ваша система всё равно будет работать. Если вам не критична скорость — ваша задача решена.
            Но в случае инфобеза — ликбез и какие-то защиты от дурака должны продвигаться агрессивней, просто потому что бреши в безопасности наносят вполне ощутимый вред. Импакт очень значительный.

            Но в целом про «звоночек» вы правы. Я просто считаю, что с учётом важности инфобеза — этим в том числе должно заниматься комьюнити.


      1. reji
        11.05.2015 00:11

        [ sarcasm on ]
        Обоже, Яндекс и Google нанимают слабых айтишников!
        [ sarcasm off ]


        1. armab
          11.05.2015 00:33

          Не надо троллить. Там цепочка для поддержки старых браузеров


      1. armab
        11.05.2015 01:22
        +1

        Проблема достаточно большая.

        Интерактивная инфографика со средней температурой по интернету для топ 1М сайтов:
        www.trustworthyinternet.org/ssl-pulse

        У четверти сайтов - мусорный F рейтинг
        image


    1. grossws
      10.05.2015 21:18

      Ещё можно добавить server_tokens off; в http/server, чтобы заголовок Server: nginx/1.x.y отдавался без версии (Server: nginx).


  1. kireevco
    10.05.2015 06:02
    +7

    Расскажите, кто пользовался HAProxy и nginx в качестве балансировщика, какие преимущества HAProxy имеет перед nginx?


    1. vaniaPooh
      10.05.2015 08:18
      -2

      Если я не ошибаюсь, то Nginx в бесплатной версии не умеет делать активную балансировку, т.е. если реплика завалилась, то пользователь это увидит с определенной вероятностью.


      1. kireevco
        10.05.2015 08:54
        +1

        Читаю документацию по бесплатной версии:

        Reverse proxy implementation in nginx includes in-band (or passive) server health checks. If the response from a particular server fails with an error, nginx will mark this server as failed, and will try to avoid selecting this server for subsequent inbound requests for a while.

        The max_fails directive sets the number of consecutive unsuccessful attempts to communicate with the server that should happen during fail_timeout. By default, max_fails is set to 1. When it is set to 0, health checks are disabled for this server. The fail_timeout parameter also defines how long the server will be marked as failed. After fail_timeout interval following the server failure, nginx will start to gracefully probe the server with the live client’s requests. If the probes have been successful, the server is marked as a live one.


        То есть если health check не прошел, трафик перестает идти на эту ноду.


        1. AlexWinner
          10.05.2015 10:38
          +4

          Вот как раз в вашей цитате написано: «passive» health checks. То есть, запрос от пользователя пойдет на упавшую ноду, провисит там до таймаута, и потом nginx может переслать его на здоровую ноду. Таким образом, пользователь будет ждать всё это время таймаута. (После этого да, nginx может пометить ноду как упавшую).
          В то время как varnish и haproxy умеют делать active health checks — они сами посылают раз в какое-то время запросы на ноды, чтобы проверить их.


          1. kireevco
            10.05.2015 10:39
            +2

            Да, точно. Ключ в 'in-band (or passive)'. Спасибо!
            Есть github.com/yaoweibin/nginx_upstream_check_module, wiki.nginx.org/HttpHealthcheckModule и еще пара модулей которые должны реализовать активные проверки, никто не использует, случайно?


          1. Alukardd
            10.05.2015 15:16
            +5

            На нагруженном проекте, это не важно, т.к. запрос от пользователя придёт с большой долей вероятности раньше чем отработает active check…


            1. trublast
              10.05.2015 19:42

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


        1. kemko
          10.05.2015 10:40
          +1

          И не только. Если один бэкэнд провалил запрос — его можно попытаться всё-таки выполнить на другом: nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
          Это, конечно, не полноценный health check, но уже что-то.


    1. n1nj4p0w3r
      10.05.2015 08:38
      +1

      Что вспомнилось из практики:
      — proxy protocol для tcp, что позволяет с удобствами балансировать тот-же postfix
      — kerberos/ntlm при L7 балансировке
      — haproxy умеет RPC over HTTP v1 и RPC over HTTP v2, что позволяет ему выполнять L7 балансировку до exchange rpc over http (outlook anywhere) и rdp over http (MS Terminal Services Gateway)

      Упоминая L7 балансировку я прежде всего я имею ввиду ssl offloading, а уж потом игры с заголовками, куками и т.д.


      1. kireevco
        10.05.2015 09:01

        Про TCP и Kerberos/NTLM понял, спасибо.
        Но ведь nginx умеет (и умел) ssl offloading, а http заголовки и cookies (в том числе через lua) хорошо должны быть доступны?


        1. n1nj4p0w3r
          10.05.2015 09:24

          Я не имел ввиду, что nginx не умеет ssl offoading или ковыряние в заголовках, я обратил внимание на то, что ssl offloading в моих юзкейсах критичен, а kerberos/rpc over http при tcp балансировке можно получить и на nginx.


      1. Botkin
        10.05.2015 12:31

        Проксирование виндовых серверов с NTLM смог осилить только на haproxy. Может, конечно, руки кривые, но этот продукт показался мне очень дружелюбным и очевидным в настройке.
        До haproxy извращался с arr в IIS (писал на хабр об этом)


  1. onthefly
    10.05.2015 12:02
    +3

    добавление ещё одного приложения в стопку

    слово «стек» можно было и не переводить


    1. SLY_G Автор
      11.05.2015 00:23

      Иногда мозг в режиме перевода начинает переводить всё подряд. Исправил.


  1. alkresin
    10.05.2015 12:19
    +1

    Эра незашифрованного веба проходит, и это хорошо

    А что хорошего-то?


    1. ntfs1984
      10.05.2015 13:38
      -8

      А ничего.
      Те кому нужно (читайте, трехбуквенные конторы) — те при желании найдут и перехватят и с зашифрованным вебом.

      Остальные будут платить дополнительным временем (а кто на платном трафике — те и деньгами) за дополнительные рукопожатия и прочие проверки.


      1. questor
        10.05.2015 14:33
        +3

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


        1. vsb
          10.05.2015 20:34
          +2

          Не факт, что перестанут. Кто-ж им помешает делать MITM своим самоподписанным сертификатом? Ещё и инструкцию на сайте выложат по добавлению сертификата в систему, чтобы «браузер не глючил».


        1. ntfs1984
          10.05.2015 20:45
          -4

          Примерчик приведите пожалуйста из личного опыта.

          Последний раз такое наблюдал лет 10 назад на каком-то уже мертвом хостинге а-ля народ.ру.


          1. cy-ernado
            10.05.2015 21:11
            +4

            Билайн и бесплатное WiFi в метро, например.