Авторизация клиентов в nginx посредством SSL сертификатов

    Введение:


    Потребовалось мне тут как-то написать небольшой API, в котором необходимо было помимо обычных запросов принимать запросы с «высокой степенью секретности».
    Не я первый с этим столкнулся и мир давно уже использует для таких вещей SSL.

    Поскольку на моём сервере используется nginx, то был установлен модуль SSL
    Гугл не выдал ни одного работоспособного howto, но информация в сети есть по частям.

    Итак, пошаговое руководство по настройке nginx на авторизацию клиентов через SSL-сертификаты.

    Внимание! В статье для примера используются самоподписанные сертификаты!

    Перед стартом создадим папку в конфиге nginx, где будут плоды наших трудов:

    cd /path/to/nginx/config/
    mkdir ssl && cd ssl


    Шаг 1. Создание собственного самоподписанного доверенного сертификата.



    Собственный доверенный сертификат (Certificate Authority или CA) необходим для подписи клиентских сертификатов и для их проверки при авторизации клиента веб-сервером.
    С помощью приведенной ниже команды создается закрытый ключ и самоподписанный сертификат.

    openssl req -new -newkey rsa:1024 -nodes -keyout ca.key -x509 -days 500 -subj /C=RU/ST=Moscow/L=Moscow/O=Companyname/OU=User/CN=etc/emailAddress=support@site.com -out ca.crt


    Описание аргументов:

    req Запрос на создание нового сертификата.
    -new Создание запроса на сертификат (Certificate Signing Request – далее CSR).
    -newkey rsa:1023 Автоматически будет создан новый закрытый RSA ключ длиной 1024 бита. Длину ключа можете настроить по своему усмотрению.
    -nodes Не шифровать закрытый ключ.
    -keyout ca.key Закрытый ключ сохранить в файл ca.key.
    -x509 Вместо создания CSR (см. опцию -new) создать самоподписанный сертификат.
    -days 500 Срок действия сертификата 500 дней. Размер периода действия можете настроить по своему усмотрению. Не рекомендуется вводить маленькие значения, так как этим сертификатом вы будете подписывать клиентские сертификаты.
    -subj /C=RU/ST=Moscow/L=Moscow/O=Companyname/OU=User/CN=etc/emailAddress=support@site.com
    Данные сертификата, пары параметр=значение, перечисляются через ‘/’. Символы в значении параметра могут быть «подсечены» с помощью обратного слэша «\», например «O=My\ Inc». Также можно взять значение аргумента в кавычки, например, -subj «/xx/xx/xx».

    Шаг 2. Сертификат сервера


    Создадим сертификат для nginx и запрос для него
    openssl genrsa -des3 -out server.key 1024
    openssl req -new -key server.key -out server.csr


    Подпишем сертификат нашей же собственной подписью
    openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt


    Чтобы nginx при перезагрузке не спрашивал пароль, сделаем для него беспарольную копию сертификата
    openssl rsa -in server.key -out server.nopass.key


    Конфиг nginx

    listen *:443;
    ssl on;
    ssl_certificate /path/to/nginx/ssl/server.crt;
    ssl_certificate_key /path/to/nginx/ssl/server.nopass.key;
    ssl_client_certificate /path/to/nginx/ssl/ca.crt;
    ssl_verify_client on;
    
    keepalive_timeout 70;
    fastcgi_param SSL_VERIFIED $ssl_client_verify;
    fastcgi_param SSL_CLIENT_SERIAL $ssl_client_serial;
    fastcgi_param SSL_CLIENT_CERT $ssl_client_cert;
    fastcgi_param SSL_DN $ssl_client_s_dn;


    теперь сервер готов принимать запросы на https.
    в переменных к бекенду появились переменные с информацией о сертификате, в первую очередь SSL_VERIFIED (принимает значение SUCCESS).

    Однако если вы попытаетесь зайти на сайт, он выдаст ошибку:
    400 Bad Request
    No required SSL certificate was sent


    Что ж, логично, в этом-то и вся соль!

    Шаг 3. Создание клиентских сертификатов



    3.1 Подготовка CA


    Создадим конфиг
    nano ca.config

    со следующим содержимым:
    [ ca ]
    default_ca = CA_CLIENT # При подписи сертификатов # использовать секцию CA_CLIENT
    
    [ CA_CLIENT ]
    dir = ./db # Каталог для служебных файлов
    certs = $dir/certs # Каталог для сертификатов
    new_certs_dir = $dir/newcerts # Каталог для новых сертификатов
    
    database = $dir/index.txt # Файл с базой данных подписанных сертификатов
    serial = $dir/serial # Файл содержащий серийный номер сертификата (в шестнадцатеричном формате)
    certificate = ./ca.crt # Файл сертификата CA
    private_key = ./ca.key # Файл закрытого ключа CA
    
    default_days = 365 # Срок действия подписываемого сертификата
    default_crl_days = 7 # Срок действия CRL
    default_md = md5 # Алгоритм подписи
    
    policy = policy_anything # Название секции с описанием политики в отношении данных сертификата
    
    [ policy_anything ]
    countryName = optional # Поля optional - не обязательны, supplied - обязательны
    stateOrProvinceName = optional
    localityName = optional
    organizationName = optional
    organizationalUnitName = optional
    commonName = optional
    emailAddress = optional


    Далее надо подготовить структуру каталогов и файлов, соответствующую описанной в конфигурационном файле

    mkdir db
    mkdir db/certs
    mkdir db/newcerts
    touch db/index.txt
    echo "01" > db/serial


    3.2. Создание клиентского закрытого ключа и запроса на сертификат (CSR)


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

    openssl req -new -newkey rsa:1024 -nodes -keyout client01.key -subj /C=RU/ST=Moscow/L=Moscow/O=Companyname/OU=User/CN=etc/emailAddress=support@site.com -out client01.csr


    В результате выполнения команды появятся два файла client01.key и client01.csr.

    3.3. Подпись запроса на сертификат (CSR) с помощью доверенного сертификата (CA).


    При подписи запроса используются параметры заданные в файле ca.config

    openssl ca -config ca.config -in client01.csr -out client01.crt -batch


    В результате выполнения команды появится файл клиентского сертификата client01.crt.

    Для создания следующих сертификатов нужно повторять эти два шага.

    3.4. Создание сертификата в формате PKCS#12 для браузера клиента

    Это на тот случай, если к вашему серверу подключаются не бездушные машины, как в моём случае, а живые люди через браузер.
    Запароленный файл PKCS#12 надо скормить браузеру, чтобы он смог посещать ваш сайт.

    openssl pkcs12 -export -in client01.crt -inkey client01.key -certfile ca.crt -out client01.p12 -passout pass:q1w2e3


    3.5 Подключение к полученному ssl cерверу с помощью curl

    curl -k --key client.key --cert client1.crt --url "https://site.com"


    Использована опция -k, потому что сертификат в примере самоподписанный

    Использованное ПО


    Ubuntu Server 10.10 (Linux 2.6.35-22-server #35-Ubuntu SMP x86_64 GNU/Linux)
    nginx 0.9.3
    OpenSSL 0.9.8o 01 Jun 2010

    Полезные ссылки




    Надеюсь, был кому-то полезен.

    P.S. Этот пост был моей первой публикацией 16 января 2011 года, старая копия удалена (по желанию администрации сайта и потому, что она не была привязана к моему аккаунту).
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

      +3
      Ну, во-первых, с openssl поставляются скрипты, делающие почти всю черную работу за вас.

      В файле конфигурации сертификатов (как правило, файл называется openssl.cnf, в вашем случае ca.conf, кстати, тоже непонятно, зачем придумывать стандартным конфигам свои имена файлов) была отмечена опция default_crl_days = 7 как Срок действия CRL. Но, при этом, остальные опции CRL отсутствуют (например, директория отозванных сертификатов, следующий порядковый номер отзыва) и, вообще в статье не отмечена очень важная возможность отзывов сертификатов.

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

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

        Спасибо за внимательность!
          0
          Дописать в том же духе за давностью лет тяжело, поэтому добавлю в комментарии, а не в основной текст:

          в nginx можно дописать опцию проверки отозванных сертификатов, выглядит она вот так:
          ssl_crl /path/to/nginx/conf/crl.pem;


          Как сгенерировать файл с отозванными сертификатами статьи уже есть и не одна.
            0
            Ну вы просто не забудьте отметить этот тонкий момент. Потому что про него все забывают и нигде заранее не пишут. Я не пользовался Ubuntu, осваивал openssl на FreeBSD 6 по исходникам. А первый раз получил задание от директора отозвать сертификат менеджера за то время, пока тот не добежит бегом до компьютера, чтобы не слить клиентскую БД. Обычно, в случаях отзывов сертификатов, счет идет на секунды! Важно готовить скрипты заранее, чтобы можно было из любого окна терминала ограничить доступ любому сертификату.

            А если вы не подготовите команды или скрипты заранее, возможна и такая ситуация, когда надо будет переделывать все сертификаты из-за того, что не могли отозвать один.
        0
        К сожалению, тут у Nginx есть небольшая проблема — вы не сможете сделать в рамках одного блока server одновременно открытую (не требующую проверки сертификатов) и закрытую (с проверкой) часть сайта или интерфейса. И разработчики это менять не собираются.
        В таком случае нужно делать SSL-авторизуемую часть или на другом домене, или открытую часть делать вовсе без SSL или вообще ставить Apache (он такое умеет).
          0
          На форумах nginx'а есть workaround. ssl_verify_client выставляется в optional и в нужных location проверяется переменная $ssl_client_verify, см ответ Игоря (http://forum.nginx.org/read.php?29,173747,173809#msg-173809). Но, к сожалению, костыли.
            0
            Надо проверять, конечно, но при такой настройке браузер может быть будет спрашивать сертификат у пользователя при первом же посещении даже на открытой странице (например, на главной). Это может быть нежелательно.
              +1
              Будет, если есть сертификаты, подписанные теми CA, которые указаны в рамках секции Certificate Request в server hello.

              Т. е., если у пользователя нет сертификатов, подписанных теми CA, которые указаны в ssl_client_certificate, то в firefox и chrome (по крайней мере, на современных) пользователь не получит запроса на указание сертификата. Иначе — попросят указать сертификат (или отказаться, но тогда для входа по сертификату придется перезапустить браузер или зайти в порно-режиме).

              Потому и говорю, что костыли.
                0
                Подскажите, а как сравниваются CA? по хешу ключа или текстовым данным?
                Большинство CA генерируется по одной и той-же статье. Есть ли шанс что у постороннего человека с ключами для другого сайта будет выскакивать окошко на нашем?
                  0

                  Смотрим в https://tools.ietf.org/html/rfc5246#section-7.4.4 и видим, что в CertificateRequest отправляется набор DNов CA. Так что если DN вашего самоподписанного CA будет тупо /CN=Example CA или /CN=Root CA, то проблемы будут.


                  Название distinguished name кагбэ говорит, что оно должно быть отличным от других (уникальным).

          0
          сколько ни пытался — та же фигня, 400 No required SSL certificate was sent
            0
            Ну так надо добить — отправить валидный сертификат :)
            Подозреваю, ошибочка то в браузере выдана была, а браузер точно свой сертификат видел? :)
            То есть самый просто вариант — curl, как у автора. Для проверки и минимизации ошибок — самое то.
            0
            Нашел статью, сделал все по шагам но браузер пишет «No required SSL certificate was sent».
            Задача — поднять сайт с https на локалхосте (чисто в целях обучения).
              0

              А через curl работает? Корневой сертификат, которым подписан клиентский сертификат, добавлен в конфиг nginx как опция ssl_trusted_certificate/ssl_client_certificate?

                0
                Через curl тоже самое выдает.
                Пошаговая инструкция это хорошо, но я просто не понимаю как все это работает и какой из 14 сгенерированных файлов за что отвечает… надо найти что-то еще почитать:)
                  0

                  Промазал и ответил ниже.

              +1

              Вот пошаговый мануал: по факту нужно не 14 файлов, а меньше: ключевая пара удостоверяющего центра, ключевая пара сервера, ключевая пара клиента и конфиг Nginx. 7 :-)


              1. Создание сертификатов УЦ


                openssl genrsa -out ca.key 2048
                openssl req -new -x509 -days 3650 -key ca.key -out ca.crt

              2. Создание сертификата сервера


                1. Конфигурационный файл server.cnf, пример:


                  [req]
                  prompt = no
                  distinguished_name = dn
                  req_extensions = ext
                  
                  [dn]
                  CN = Сервер с аутентификацией клиентов
                  emailAddress = my.email@example.com
                  O = Private person
                  OU = Alter ego dept
                  L = Korolyov
                  C = RU
                  
                  [ext]
                  subjectAltName = DNS:example.com,DNS:*.example.com,IP:127.0.0.1

                  В subjectAltName должны быть указаны все используемые DNS-имена и IP-адреса.


                2. Создание запроса на сертификат


                  openssl req -new -utf8 -nameopt multiline,utf8 -config server.cnf -newkey rsa:2048 -keyout server.key -nodes -out server.csr

                3. Создание сертификата


                  openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -extfile server.cnf -extensions ext

                4. Настройка Nginx, пример (фрагмент)


                  http {
                    ssl_certificate         /path/to/server.crt;
                    ssl_certificate_key     /path/to/server.key;
                    ssl_client_certificate  /path/to/ca.crt;
                    ssl_verify_client       on;
                  
                    server {
                      listen       8443 ssl default_server;
                      listen       [::]:8443 ssl default_server;
                    }
                  }


              3. Создание сертификата клиента


                openssl req -new -utf8 -nameopt multiline,utf8 -newkey rsa:2048 -nodes -keyout client.key -out client.csr
                openssl x509 -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

              4. Проверка


                Следующая команда должна отрабатывать без ошибок:


                curl -v --cacert path/to/ca.crt --cert path/to/client.crt --key path/to/client.key https://127.0.0.1:8443/

                Следующая команда должна возвращать ошибку «не предоставлен сертификат клиента»:


                curl -v --cacert path/to/ca.crt https://192.168.194.97:8443/

              5. Добавление сертификата ЦА в RHEL (от имени root, где path/to/ca.crt — путь к файлу корневого сертификата)


                cp path/to/ca.crt /etc/pki/ca-trust/source/anchors/
                update-ca-trust


              Последний пункт нужен, чтобы можно было не указывать явно сертификат ЦА в командах (в том же cURL).

                0

                Одна маленькая ремарка: лучше в серверном CN указывать хост. В SAN он тоже должен присутствовать, естественно.

                  0

                  Вот уж пару лет развлекаюсь тем, что пихаю в CN надписи на русском (в UTF-8) — пока проблем ни с каким софтом не возникало — все смотрят в SubjectAltNames.
                  Впрочем, раньше я это делал чисто из эстетических соображений — чтобы видеть эти русские буквы в информации о сертификате, сейчас же, когда в хроме надо пройти целый квест, чтобы эту информацию увидеть, большая часть смысла пропала :-)

                  0
                  Спасибо! Все тесты с curl проходят нормально, а через браузер все равно не работает.
                  Система Lubuntu, поэтому добавление сертификата не такое как в пункте 5, там просто нет ca-trust внутри /etc/pki и команды update-ca-trust.
                  Делал так (в целом это похожие варианты)
                  http://manuals.gfi.com/en/kerio/connect/content/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html
                  https://askubuntu.com/questions/73287/how-do-i-install-a-root-certificate
                  https://unix.stackexchange.com/questions/90450/adding-a-self-signed-certificate-to-the-trusted-list
                    +1

                    А вы добавляли корневой сертификат в браузер? Т.е. зелёный замочек в браузере есть? Дело может быть в этом. См. https://habrahabr.ru/post/213741/#comment_7814423, например.

                      0
                      Нет, я думал браузер из линукса сам возьмет.
                      Но добавление не помогло: теперь браузер опять выдает 400 Bad Request No required SSL certificate was sent
                        0
                        В общем директива ssl_verify_client = off; помогла. Но я совсем не уверен что это правильно и не знаю можно ли так делать на реальном сервере…
                          0

                          В смысле "помогла"? Вы просто выключили эту фичу IIRC

                            0
                            Да, это понятно что выключил… непонятно в чем там кроется собака:) Я уже пересоздал сертификаты, все параметры ввел в cnf и при вводе в консоль одинаковые буква в букву. Есть вот такое обсуждение https://forum.nginx.org/read.php?21,182573,183073#msg-183073 но там предлагают ставить патч.

                            Но я бы сначала хотел в принципе разобраться что происходит в реальной жизни (когда юзер заходит на сайт по https) и что происходит под локалхолстом с самоподписанным сертификатом.
                            Как я понимаю, «ca» — это сертификат удостоверяющего центра (в реальной системе у меня его не будет, а будут вшитые в браузер); «server» — это сертификат сервера, то что мне выдадут например на let's encrypt для моего домена. А что такое сертификат клиента «client»? Откуда он берется в реальных условиях (браузер открывает https ссылку) и почему мы его генерируем для локалхоста?
                              +3

                              Если на пальцах, то в случае использования клиентской аутентификации по tls есть две ветки PKI.


                              1. Сторона сервера (которая есть и без клиентской authN), которая включает root CA, intermediate CAs и серверный сертификат. При подключении клиента к серверу при установлении соединения сервер отдаёт свой сертификат с реверсной цепочкой не включая CA (т. к. он есть у пользователя). Файл с этими сертификатами в nginx указывается в параметре ssl_certificate.
                              2. Сторона клиента имеет независимую ветку PKI (которая может спокойно использовать приватный CA). При включенной ssl_verify_client (необходимо также указать ssl_client_certificate, если используется on или optional) сервер после ServerHello передаёт дополнительно фрейм Certificate Request, в котором перечислены dn'ы CA которым данный endpoint доверяет в контексте аутентификации клиента (в случае использования optional_no_ca этот список пустой). Клиент на этот запрос предоставляет сертификат (в случае браузера показывает окошко, где пользователь выбирает какой сертификат использовать).

                              И первая ветка PKI (серверная) в нормальном случае получается через публичный CA типа LetsEncrypt. Сторона же пользователя, в зависимости от сервиса может получаться:


                              • от публичного или государственного CA,
                              • от приватного CA самого сервиса (который, по сути, в статье и описан).

                              Если хочется разобраться, то рекомендую взять для начала wireshark, открыть rfc5246, выполнить описанное в 3 части статьи.


                              Я для тестов использовал сейчас более простую конфигурацию:


                              пример
                              # генерируем ключ
                              openssl genrsa -out ca.pem 1024
                              # генерируем самоподписанный сертификат
                              openssl req -x509 -subj '/CN=test ca' -days 365 -key ca.pem -out ca.crt
                              # смотрим глазами на самоподписанный (root) CA
                              openssl x509 -in ca.crt -noout -text
                              
                              # генерируем клиентский ключ
                              openssl genrsa -out client.pem 1024
                              # генерируем csr и приносим его на машину с CA
                              openssl req -key client.pem -new -out client.csr -subj '/CN=client'
                              
                              # генерируем клиентский сертификат на основе csr
                              openssl x509 -req -in client.csr -out client.crt -CA ca.crt -CAkey ca.pem -CAcreateserial
                              # смотрим на него
                              openssl x509 -in client.crt -noout -text
                              # собираем на машине с ключом pkcs12 контейнер с ключом и сертификатом для браузера
                              openssl pkcs12 -export -out client.p12 -in client.crt -inkey client.pem
                              
                              # !!! импортируем client.p12 в браузер
                              # прописываем в уже работающем по https nginx'е `ssl_client_verify` on, optional или optional_no_ca.  в первых двух случаях не забываем притащить nginx'у ca.crt и прописать в `ssl_client_certificate`
                              
                              # пользуемся

                              Для отладки ssl/tls chrome (и, вроде, ff) поддерживают переменную окружения SSLKEYLOGFILE, в которой указывается файл куда сохранять сессионные ключи. Эту переменную можно использовать только при отладке. При наличии этого файла кто угодно может расшифровать содержание SSL/TLS сессий (в том числе при использовании PFS).


                              Я использую следующий скрипт-обёртку:


                              #!/bin/bash
                              
                              export SSLKEYLOGFILE=~/.cache/tls-premaster 
                              
                              google-chrome-stable --user-data-dir=/tmp/tls-debug --incognito
                              rm -rf /tmp/tls-debug
                              shred -u ~/.cache/tls-premaster

                              Путь к этому файлу нужно указать wireshark'у в preferences -> protocols -> ssl, пункт (Pre)-Master-Secret log filename.

                                +2

                                Вот ещё хорошая статья, если хочется знать, что там под капотом и как работает: https://tls.dxdt.ru/tls.html


                                Ещё могу посоветовать стэнфордский курс Crypto на Coursera — хорошо объясняют про криптографию в целом (но уже чёрт знает сколько лет не могут выпустить вторую часть курса).

                        +3
                        3.5 Подключение к полученному ssl cерверу с помощью curl

                        curl -k --key client.key --cert client1.crt --url "https://site.com"

                        Использована опция -k, потому что сертификат в примере самоподписанный


                        зашёл сказать, что не надо игнорировать проверку валидности (тот самый ключ -k), но в комментируемом комменте уже есть правильный рецепт:
                        curl ... --cacert path/to/ca.crt
                        на что и хотел бы обратить внимание
                        +1
                        а известно как аутентифицировать пользователя по его (клиентскому) сертификату?
                        Т.е. подписали мы клиентских сертификатов, сервер их пускает, но нам нужно определить кто есть кто

                        в IIS, знаю, это настраивается так
                        есть что-то подобное для nginx?
                          +2

                          Когда аутентификация клиента проходит успешно, то Nginx заполняет несколько переменных данными о клиентском сертификате: https://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables
                          Эти переменные уже можно передать в заголовках приложению и оно там будет искать клиента у себя. Вам скорее всего нужен $ssl_client_escaped_cert, $ssl_client_s_dn или $ssl_client_serial

                            0
                            благодарю!
                            очевидно, это то, что нужно

                        Only users with full accounts can post comments. Log in, please.