Всем привет. Данная статья расчитана на тех, кто хочет сделать так, чтобы их Spring Boot приложение могло работать с HTTPS без предупреждений со стороны браузера о небезопасном подключении. В этой статье работаем именно со Spring Boot и вшитым в него Apache Tomcat.

Внимание! Статья игнорирует всевозможные правила безопасноти Linux и призвана показать как достичь элементарной работы с HTTPS в Spring Boot, не сильно углубляясь в смежные темы.

Рисунок 1 - ругань браузера при переходе на HTTPS с помощью самописных сертификатов
Рисунок 1 - ругань браузера при переходе на HTTPS с помощью самописных сертификатов

От вас требуется:

  1. Spring приложение на Linux сервере (например Ubuntu 20+), на котором у вас есть доступ к root правам

  2. Действующие ендпоинты, например, @GetMapping

  3. Базовое представление о принципах работы HTTPS

  4. Знакомство с системой сборки Maven

Вообще, можно использовать HTTPS в Spring приложении и с помощью self-signed сертификата, который генерируется с помощью утилиты keytool, вшитой прямо в JDK, но вызовы ваших ендпоинтов с таким сертификатом будут вызывать предупреждение в браузере, как на рисунке 1.

Чтобы наше приложение могло работать с HTTPS без ругани со стороны браузера нам нужен "сертификат", подписанный CA (Certification Authority).

Ни вы, дорогой читатель, ни я не являемся Certification Authority. Ваш браузер или браузер любого другого пользователя знать не знает, что за кустарные (палёные) сертефикаты мы тут генерируем с помощью keytool! Отсюда и предупреждение о небезопасном соединении.

Чтобы сгенерировать сертификат, который будет валидым и безопасным в глазах всех браузеров нам не обойтись без помощи вышеупомянутого CA. Кто-то может купить сертификат у провайдеров облачных серверов, но у меня нет лишних денег поэтому в этой статье мы воспользуемся сертефикатом от Let's Encrypt, который будет нам выдан через Certbot'a.

Важно понимать, что Let's Encrypt - это Certification Authority (nonprofit организация), сертефикаты который являются валидными в глазах браузеров, а выдавать их сертефикат нам будет утилита под названием Certbot.
Взаимосвязанны ли они? Да. Являются ли они одним и тем же? Нет.

Для примера можно привести HTTPS сертефикат Stack Overflow, они тоже используют Let's Encrypt.

Рисунок 2 - услугами Let's Encrypt пользуются Stack Overflow
Рисунок 2 - услугами Let's Encrypt пользуются Stack Overflow

Собственно сам туториал

Еще раз напомню, что у вас уже должен быть живой Spring Boot проект, который работает через HTTP и имеет какое-то количество ендпоинтов. Чтобы начать работать с HTTPS нам надо выполнить 3 задачи:

  1. Настроить firewall

  2. Правильно сгенерировать сертификат

  3. Настроить Spring Boot

1. Настройка firewall

Firewall мы настроим через ufw, но это не панацея и вы можете использовать свои инструменты. Установим ufw

sudo apt install ufw

Откроем интересующие нас порты:

sudo ufw allow 80; 
sudo ufw allow 443; 
sudo ufw allow 8080;
sudo ufw allow ssh;

Внимание! следующая команда заблокирует любой входящий трафик. Это значит, что все порты будут закрыты, пока вы их не откроете, например, через sudo ufw allow. Если вы на удаленном сервере обязательно выполните sudo ufw allow ssh, иначе рискуете заблокировать себе доступ к серверу.

sudo ufw enable

Чтобы проверить способность вашего сервера принимать запросы после ufw конфигурации, запустите приложение и попробуйте отправить любой запрос на: http://<ваш_ipv4_адрес>:8080

Наглядный пример URL запроса с портом: http://90.156.210.6:8080

Имея @RestController класс с таким методом:

@RestController
public class DuelController {
  
  @GetMapping     
  public String test(){       
    return "Transition to https";   
  }
  
}

Мы можем достучаться до нашего сервера по порту 8080, как на рисунке 3.

Рисунок 3 - проверяем доступность портов
Рисунок 3 - проверяем доступность портов

Если ваш ендпоинт не отзывается, проверьте открыт ли порт, по которому вы обращаетесь к серверу с помощью sudo ufw status

Мы хотим увидеть следующий вывод:

habr@personal_computer:~/demo$ sudo ufw status 
Status: active 
To                         Action      From
22/tcp                     ALLOW       Anywhere 
443                        ALLOW       Anywhere 
80                         ALLOW       Anywhere 
8080                       ALLOW       Anywhere 
22/tcp (v6)                ALLOW       Anywhere (v6) 
443 (v6)                   ALLOW       Anywhere (v6) 
80 (v6)                    ALLOW       Anywhere (v6)
8080 (v6)                  ALLOW       Anywhere (v6) 

2. Правильная генерация сертификата

Подключитесь к своему Linux серверу и в терминале начните с проверки наличия на сервере snapd:

sudo systemctl status snapd

Если вы видите вывод как на рисунке 4 (active (running)...), то все в порядке и можно двигаться дальше. Если нет, то вам надо установить snap или перезапустить его daemon.

Рисунок 4 - вывод, который говорит, что daemon snapd в данный момент работает
Рисунок 4 - вывод, который говорит, что daemon snapd в данный момент работает

Установим саму утилиту Certbot:

sudo snap install --classic certbot

Теперь создадим symbolic link, так нас просит сделать инструкция Certbot'a:

sudo ln -s /snap/bin/certbot /usr/bin/certbot

Теперь убедитесь, что ваше Spring приложение остановлено. Следующая команда интерактивная и требует заранее подготовленной информации, а конкретно:

  1. Email. Какой хотите, но в инструкции сказано, что использоваться он будет для оповещений по вопросам безопастности и срочного продлевания сертификата

    Enter email address (used for urgent renewal and security notices)

  2. Валидное доменное имя, привязанное к вашему IPv4 адрессу. Вам надо купить (или каким-нибудь способом достать) доменное имя, например, vashdomen.ru и привязать его к вашему серверу.
    Без привязанного к вашему серверу домена вы не сгенерируете сертификат. Certbot выдаст вам ошибку.

    Вы можете купить самый простецкий домен за "сотку" у рег.ру (не рефералка) и по их инструкции привязать IP адресс вашего сервера к их DNS серверам. Если вы укажите другие DNS сервера, то и настройку надо будет делать в другом месте. Возможно на сайте владельцев этих DNS серверов.

    Процесс привязки домена может занять некоторое время.

    Чтобы протестировать привязался ли домен к адресу, попробуйте выполнить следующую команду: ping vashdomen.ru
    Возможно ping отзовется не так быстро, как обычно, стоит чутка подождать.

С Email и рабочим доменным именем начинаем генерацию сертификата. Вас попросят согласится с парой пунктов, указать почту и доменное имя, которое вы привязали к серверу.

sudo certbot certonly --standalone

Если все хорошо, то вас встретит сообщение об успешной генерации файлов:

...
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/<доменное_имя>/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/<доменное_имя>/privkey.pem
This certificate expires on 2024-07-30.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
...

Самая важное тут - это пути к вашим новым файлам:

/etc/letsencrypt/live/<доменное_имя>/

Прошу обратить внимание, что хоть нам и выводится путь лишь к двум файлам по факту их сгенерировалось пять (5). Чтобы посмотреть содержимое этой директории нам нужны root права:

sudo su

Заглянем в директорию:

 ls /etc/letsencrypt/live/<доменное_имя>

Вам должен выпасть вот такой список:

cert.pem  chain.pem  fullchain.pem  privkey.pem  README

Вы сгенерировали сертификат, подписанный CA Let's Encrypt. Этот сертификат принимается браузерами за валидный и на него не падают предупреждения! Вы можете применить его и за пределами Spring приложения.

3. Настройка Spring Boot

Теперь надо познакомить ваше приложение с сертификатом. Для этого нам нужна команда openssl. Если ее нет, то установите:

 sudo apt install openssl

Перед тем как двигаться дальше вам надо перейти в директорию вашего Spring приложения. Так чтобы при выводе ls вы видели ваши src, target, pom.xml...

habr@personal_computer:~/demo$ ls
HELP.md  mvnw  mvnw.cmd  pom.xml  src  target

Теперь не торопитесь и внимательно прочитайте следующую команду. Укажите валидные пути к сгенерированным файлам. Эта команда тоже интерактивная и вы будете создавать пароль к хранилищу. Запомните его! В рамках этого туториала можете использовать пароль "test_tomcat".

sudo openssl pkcs12 -export -chain -inkey /etc/letsencrypt/live/<доменное_имя>/privkey.pem -CAfile /etc/letsencrypt/live/<доменное_имя>/fullchain.pem -in /etc/letsencrypt/live/<доменное_имя>/cert.pem -out keystore.p12 -name tomcat 

Теперь, когда у вас на руках есть пароль и хранилище приватных ключей, которое лежит в корневой директории, рядом с pom.xml, в самом Spring надо отредактировать application.properties

Вставьте туда следующие строчки:

server.port = 443 
server.ssl.key-store = keystore.p12 
server.ssl.key-store-password = test_tomcat 
server.ssl.keyStoreType = PKCS12
server.ssl.keyAlias = tomcat

Объясняю:

  1. server.port - порт, на котором будет работать ваше приложение. HTTPS работает через порт 443

  2. server.ssl.key-store - название хранилища, которое мы сгенерировали (keystore.p12)

  3. server.ssl.key-store-password - пароль, который вы вводили при генерации хранилища и который надо было запонмить

  4. server.ssl.keyAlias - это флаг (-name tomcat), который мы указали в команде при генерации хранилища

Финальное тестирование

Проверим нужные порты:

sudo ufw status

Видим вывод:

habr@personal_computer:~/demo$ sudo ufw status 
Status: active 
To                         Action      From
22/tcp                     ALLOW       Anywhere 
443                        ALLOW       Anywhere 
80                         ALLOW       Anywhere 
8080                       ALLOW       Anywhere 
22/tcp (v6)                ALLOW       Anywhere (v6) 
443 (v6)                   ALLOW       Anywhere (v6) 
80 (v6)                    ALLOW       Anywhere (v6)
8080 (v6)                  ALLOW       Anywhere (v6) 

Порты подготовлены.

Порт 22 - это ssh соединение, а (v6) является обозначением для IPv6.

Итак, мы:

  1. Порты настроили

  2. Сертификат сгенерировали

  3. Spring Boot приложение с сертификатом познакомили

Пора запускать.

Перед тем как запускать приложение надо понимать, что первые 1024 порта зарезервированы и для запуска приложений, которые общаются с этими портами нам нужны root права. Наше приложение будет работать на порте 443, а значит запуск Spring Boot требует повышенных прав.

Укажите полный путь до бинарного файла Maven, чтобы терминал распознал команду через sudo:
Или можете отредактировать PATH для root пользователя.

sudo /home/habr/apache-maven-3.9.6/bin/mvn spring-boot:run

В информационных логах Spring нас интересует 3 лога.

Первый лог говорит нам о том, что Tomcat запустился на порте 443

INFO 19474 --- [demo] [main] o.s.b.w.embedded.tomcat.TomcatWebServer:
Tomcat initialized with port 443 (https)

Второй лог выводит нам информацию о хранилище приватных ключей

INFO 19474 --- [demo] [main] o.a.t.util.net.NioEndpoint.certificate: 
Connector [https-jsse-nio-443], TLS virtual host [default], certificate type [UNDEFINED] configured from keystore [/root/.keystore] using alias [tomcat] with trust store [null]

Третий лог дополняет первый и показывает нам базовый context path. По дефолту "/"

 INFO 19474 --- [demo] [main] o.s.b.w.embedded.tomcat.TomcatWebServer: 
Tomcat started on port 443 (https) with context path ''

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

Рисунок 5 - наш сертефикат verified by: Let's Encrypt
Рисунок 5 - наш сертефикат verified by: Let's Encrypt

Если все прошло успешно, вы можете обращаться к ендпоинтам вашего Spring Boot приложения через протокол HTTPS без ругани со стороны бразуера.

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


  1. ky0
    04.05.2024 08:27
    +6

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


    1. Kirillchug
      04.05.2024 08:27

      Ну тут не только для локальной разработки. Тут вы как минимум уже покупаете домен, что уже может давать путь в открытый интернет и выхода в прод для своего проекта. А HTTPS браузерами "поощряется", и это отличный SEO подход для выхода в топ-10


      1. GennPen
        04.05.2024 08:27

        А HTTPS браузерами "поощряется"

        Не только "поощряется", но и принуждает из-за HSTS например, или просто на сервере настроено правило.


  1. iliabvf
    04.05.2024 08:27

    Самый просто способ для меня был Cloudflare, бэкенд на субдомен с https, фронт на домен с https. Бесплатно, красиво имеем защиту, кэш фильтры.
    Можно конечно и все selfhosted.


    1. Boilerplate
      04.05.2024 08:27
      +1

      Только в РФ ip-адреса CF периодически блочат, к сожалению. Из-за этого от CF приходится отказываться в случаях, когда это всякие магазины и прочие сервисы на неограниченное число людей.


  1. volatilization
    04.05.2024 08:27
    +1

    Зачем? Пусть прокси сервер слушает https с настроенными сертами. А спирнг в котейнере крутиться.


  1. mavir
    04.05.2024 08:27
    +4

    Без привязанного к вашему серверу домена вы не сгенерируете сертификат.

    Это не обязательно, есть режим генерации сертификата с использованием DNS. Всё что нужно, это иметь возможность управлять DNS записями вручную или с помощью API, если DNS хостинг предоставляет такую возможность. Для генерации сертификата через DNS даже не надо привязывать домен к IP. И еще большой плюс такой генерации - можно создать wildcard сертификат, например, для *.vashdomen.ru


    1. GennPen
      04.05.2024 08:27

      можно создать wildcard сертификат, например, для *.vashdomen.ru

      Который скормить например haproxy, через который и кидать на нужный бэкенд в зависимости от запрашиваемого домена. И это более правильное решение.

      А вот wildcard на стороне запросов DNS (чтобы не прописывать адреса для всех поддоменов) не все серверы поддерживают.


      1. mavir
        04.05.2024 08:27
        +2

        А вот wildcard на стороне запросов DNS (чтобы не прописывать адреса для всех поддоменов) не все серверы поддерживают.

        Вот если честно, то это должен быть совсем дешманский DNS хостинг, чтобы не мог поддерживать wildcard записи


      1. mavir
        04.05.2024 08:27

        А вот wildcard на стороне запросов DNS (чтобы не прописывать адреса для всех поддоменов) не все серверы поддерживают.

        Вообще я затупил. Причем тут wildcard DNS записи к генерации wildcard сертификата? При генерации сертификата через DNS нужно добавить у провайдера TXT запись, которую попросит certbot. А A-записей может вообще не быть у провайдера, их хоть в /etc/hosts добавляй вручную


        1. GennPen
          04.05.2024 08:27

          Причем тут wildcard DNS записи к генерации wildcard сертификата?

          Ну, то что не нужно на DNS создавать записи типа site1.domain site2.domain site3.domain указывающие на один и тот же IP, а можно создать *.domain и при запросе любого поддомена будет указывать на один и тот же IP.


          1. mavir
            04.05.2024 08:27

            Для генерации wildcard сертификата не нужно создавать A-записи вида *.example.com, либо любую другую для отдельных доменов. certbot просит создать TXT и вставить содержимое, которое он даст. Например,

            # certbot certonly --manual --preferred-challenges dns -d *.nekij-domen.com
            Saving debug log to /var/log/letsencrypt/letsencrypt.log
            Requesting a certificate for *.nekij-domen.com
            
            - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            Please deploy a DNS TXT record under the name:
            
            _acme-challenge.nekij-domen.com.
            
            with the following value:
            
            5pt0CXrH_icMfYXLae-jmGZV6SxZohQVWrrZ3pyYKyk
            

            Обратите внимание, что домен на текущий момент даже не зарегистрирован, но certbot предлагает создать TXT запись _acme-challenge.nekij-domen.com с содержимым 5pt0CXrH_icMfYXLae-jmGZV6SxZohQVWrrZ3pyYKyk

            Т.е. можно сгенерить wildcard сертификат, а домены создавать одиночные, не обязательно создавать wildcard DNS запись. Например, site1.example.com на одном сервере размещается, site2.example.com на другом с соответствующими A-записями, а сертификат можно один и тот же забросить на эти два сервере


            1. GennPen
              04.05.2024 08:27

              Ладно, закроем тему. Я немного непонятно выразился, вы меня не поняли. =)


  1. keekkenen
    04.05.2024 08:27
    +3

    а при чем тут Spring ?!


  1. tuxi
    04.05.2024 08:27

    Ходить на бекенд по хттпс это все же извращение. Хттпс это трата допресурсов, на хайлоаде это стрелять себе дуплетом в обе ноги сразу. Бекенд должен работать внутри периметра, а там хттпс как бы не особо нужен.

    Все истории про бекенд через хттпс которые я слышал, были связаны с вынужденным (внезапным) переносом бекенда куда то в другой ДЦ и это скорее исключение, чем норма.


    1. posledam
      04.05.2024 08:27

      HTTP/2, gRPC, не?


      1. tuxi
        04.05.2024 08:27

        gRPC не везде есть, а насчет HTTP/2 я не уверен, что он нужен бекенду, вернее что будет какой-то выигрыш


    1. ky0
      04.05.2024 08:27

      Бекенд должен работать внутри периметра, а там хттпс как бы не особо нужен.

      ...разве что вы захотите пройти аудит какой-нибудь PCI DSS :) Тогда и шифрование понадобится, и нормальная PKI, и вменяемый набор шифров.


      1. tuxi
        04.05.2024 08:27

        ну так то да


  1. olku
    04.05.2024 08:27
    +1

    Сразу три антипаттерна. Терминирование TLS (Level 6) сервисом (Level 7), отсутствие автообновления сертификата, выставление сервиса в публичный Интернет без базовой защиты от атак.