Сейчас все большее количество интернет-ресурсов и приложений декларируют полный переход на протокол передачи данных, использующий шифрование HTTPS. Более того, некоторые из них ужесточают требования к обеспечению шифрования. Теперь если вы, например, попробуете открыть ресурс, на котором был установлен самоподписанный сертификат, по зашифрованному каналу в браузере, вам могут не только вывести предупреждение о небезопасном соединении, но и пресечь попытку подключения. Все эти изменения чреваты разного рода неудобствами как для специалистов, так и для конечных пользователей.
О практической стороне применения сертификатов расскажет наш коллега Алексей Обложко. Передаем ему слово.
Создадим простейшее веб-приложение на Java и доведем его до готовности к эксплуатации в виде контейнеризированного приложения, работающего по протоколу HTTPS. Для создания приложения мы будем использовать фреймворк Jmix, который основан на Spring Boot и Vaadin, поэтому описанные подходы будут работать также для широкого класса веб-приложений на Spring Boot.
Мы предполагаем, что вы установили Docker актуальной версии для своей ОС, используя brew, chocolately или deb/rpm.
Создание приложения
Для разработки на Jmix вам нужно установить JDK 17 или 21, а также IntelliJ IDEA (достаточно Community редакции, которую можно скачать с сайта https://www.jetbrains.com/idea/download/). После запуска IDE установите в нее плагин Jmix из маркетплейса JetBrains.
Для создания нового проекта на фреймворке Jmix можно воспользоваться визардом нового проекта IntelliJ IDEA, выбрав там соответствующий тип проекта. Но мы возьмем специально подготовленный для обучения проект jmix-onboarding, в котором уже есть небольшая модель, UI, и тестовые данные.
Для его получения выберем File -> New -> Project from Version Control и в появившемся диалоге укажем адрес репозитория: https://github.com/jmix-framework/jmix-onboarding-2.git.
После того как среда разработки все скачает, соберет и проиндексирует, мы можем сразу запустить приложение, выбрав конфигурацию Jmix Application в верхней панели IDE.
Как только в логах появятся строчки про Server startup, можно открыть в браузере адрес http://localhost:8080 и авторизоваться на форме входа с логином и паролем admin.
Наша сегодняшняя задача — сделать так, чтобы в строке адреса можно было ввести https и получить то же приложение, но работающее по актуальным стандартам передачи данных.
Сертификаты для разработки
Для разработки вы можете сгенерировать самоподписанные сертификаты (например, как описано в нашей документации). Однако, они теперь не всегда работают в браузерах и других приложениях, использующих веб-протоколы. Решением для разработчика может быть выпуск собственного корневого сертификата и подписание им серверного сертификата.
Для упрощения задачи можно воспользоваться утилитой mkcert. Инструкции по установке приведены в справке проекта, который живет по адресу: https://github.com/FiloSottile/mkcert.
В результате установки в вашем Path должен оказаться исполняемый файл mkcert. Вызвав его из эмулятора терминала, мы сможем сгенерировать и установить корневой сертификат более простым путем, чем если бы мы делали это с openssl и пониманием особенностей ОС в части работы с сертификатами безопасности. Сгенерируем и установим корневой сертификат:
mkcert -install
Теперь перейдем в ресурсы проекта и сгенерируем там серверный сертификат:
mkcert -pkcs12 localhost 127.0.0.1 ::1
В качестве пароля установится changeit.
Создадим хранилище сертификатов со сгенерированным сертификатом:
keytool -importkeystore -srckeystore localhost+2.p12 -srcstoretype pkcs12 -destkeystore localhost.jks
В качестве пароля введем что-нибудь простое, например тот же changeit. Его надо будет указать в конфигурации, приведенной ниже.
Добавим конфигурацию для приложения в application.properties:
# Enables HTTPS
server.ssl.enabled=true
# The format used for the keystore
server.ssl.key-store-type = JKS
# The path to the keystore containing the certificate
server.ssl.key-store = classpath:localhost+2.jks
# The password used to generate the keystore
server.ssl.key-store-password = changeit
# The alias mapped to the certificate
server.ssl.key-alias = 1
# Changes the server's port
server.port = 8443
Теперь можно запустить приложение (или перезапустить, если вы уже успели это сделать). Когда в логах появится ссылка на https://localhost:8443, мы можем открыть ее в браузере и убедиться, что нам не показывают никаких предупреждений о небезопасном соединении.
Упаковка приложения в контейнеры
В качестве базы данных проект «из коробки» использует локальную HSQL. Это значит, что при каждом перезапуске контейнера база будет пересоздаваться. Благодаря интегрированным миграциям Liquibase схема сгенерируется автоматически. При переходе к продуктивной среде вам не составит труда поменять тип источника данных в конфигурации проекта на использование полноценной СУБД, добавив в Docker-архитектуру соответствующие контейнеры.
Что касается HTTPS, нам потребуется закомментировать всю секцию, которую мы добавляли ранее, т. к. шифрование в новых реалиях возьмет на себя фронтенд прокси-сервер.
Образ контейнера собирается командой:
./gradlew -Pvaadin.productionMode=true bootBuildImage
Выходим в продуктив
Для продуктивных серверов вам потребуется либо приобрести SSL-сертификаты и заменить ими сгенерированные, либо настроить бесплатную генерацию менее престижных сертификатов при помощи скриптов acme.sh (https://github.com/acmesh-official/acme.sh) или certbot от letsencrypt. Первый вариант чаще применяется как дополнение к контейнеризированным прокси-серверам, при помощи которого они самостоятельно получают и обновляют сертификаты, второй обычно выполняется вручную или по расписанию ОС.
Для выпуска сертификатов в общем случае необходимо иметь доменное имя и некоторый стабильный ip-адрес сервера. Доменное имя мы далее условно будем обозначать как mydomain.ru.
Настройка доменных записей
В настройках DNS надо добавить A записи @ и * указывающие на ip сервера. Проверить их работу можно командами:
nslookup mydomain.ru
Настраиваем HTTPS-прокси
Устанавливаем скрипт acme.sh:
wget -O - https://get.acme.sh | sh -s email=mymail@example.com
Некоторые регистраторы могут предоставлять свою версию скрипта. Информацию можно найти в секциях, связанных с DNS-конфигурациями админки регистратора.
На сервере ставим Docker и запускаем nginx-proxy с дополнением для интеграции acme.sh. Многие специалисты находят более удобным для описания запускаемых сервисов использовать yml-формат docker compose, но мы решили оставить для примеров вариант с аргументами командной строки.
docker run --detach \
--name nginx-proxy \
--publish 80:80 \
--publish 443:443 \
--volume certs:/etc/nginx/certs \
--volume /etc/nginx/vhost.d:/etc/nginx/vhost.d \
--volume html:/usr/share/nginx/html \
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
nginxproxy/nginx-proxy
docker run --detach \
--name nginx-proxy-acme \
--volumes-from nginx-proxy \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--volume acme:/opt/letsencrypt_acme/acme.sh \
--env "DEFAULT_EMAIL=mymail@example.com" \
nginxproxy/acme-companion
В команде выше указывается e-mail, который будет использован для получения автоматических уведомлений.
--volume acme:.. должен указывать на установленный ранее скрипт.
Разворачиваем реестр docker-образов
Вместо собственного реестра образов можно использовать DockerHub и пропустить этот шаг, используя для аутентификации его регистрационные данные. Использование собственного реестра, однако, может иметь свои преимущества: трафик в локальной сети будет ходить быстрее, что благотворно скажется на скорости выполнения ваших сборок.
Теперь, чтобы наш сервер получал разработанные нами докер-образы, необходимо развернуть его собственный реестр.
В домашнем каталоге пользователя создадим каталог с докер-проектом:
mkdir docker-registry
cd docker-registry
mkdir auth
Для генерации пароля потребуется установить в систему пакет apache2-utils. Это можно сделать родным пакетным менеджером, если у вас Linux, brew в MacOS и chocolately для Windows. В результате у нас должна начать срабатывать такая команда:
htpasswd -bnB user password > auth/htpasswd
Теперь мы готовы запустить сервис реестра docker-образов. В команде мы сразу определим параметры для nginx-proxy: VIRTUAL_PORT укажет, какой порт проксировать, а VIRTUAL_HOST и LETSENCRYPT_HOST — какой поддомен использовать. Вместо registry.mydomain.ru надо указать поддомен вашего домена.
docker container run -d -p 5000:5000 --name registry -v "$(pwd)"/auth:/auth -e REGISTRY_AUTH=htpasswd -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd -e VIRTUAL_HOST=registry.mydomain.ru -e VIRTUAL_PORT=5000 -e LETSENCRYPT_HOST=registry.mydomain.ru registry
При такой архитектуре разумно будет закрыть все порты во вне кроме стандартных для веба 80 и 443. Впрочем, обычно так всегда и делается.
Увеличим также размер запроса для того, чтобы наши docker-образы пролазили через ограничения прокси-сервера:
{ echo 'client_max_body_size 5000m;'; } > /etc/nginx/vhost.d/registry.mydomain.ru
После изменения конфигурации сервис надо перезапустить:
docker restart registry
Настраиваем публикацию проектного образа
Когда реестр развернут, мы можем собрать и опубликовать образ с локального компьютера.
В конфигурацию публикации docker-образа для проекта, а именно файл build.gradle, надо добавить следующие строчки:
bootBuildImage {
imageName = "registry.mydomain.ru/jmixonboarding:0.0.1-SNAPSHOT"
publish = true
docker {
publishRegistry {
url = "https://registry.mydomain.ru/"
username = "user"
password = "password"
}
}
}
После этого можно собрать и опубликовать docker-образ:
./gradlew bootBuildImage -Pvaadin.productionMode=true
Если мы соберем и опубликуем новую версию на клиентах, надо не забывать делать docker pull.
На сервере авторизуемся в реестре:
docker login -u username –p password https://registry.mydomain.ru
Если мы соберем и опубликуем новую версию на клиентах, надо не забывать делать docker pull.
На сервере авторизуемся в реестре:
docker login -u username –p password https://registry.mydomain.ru
и запускаем образ приложения:
docker run --detach \
--name testnginx \
--env "VIRTUAL_HOST= jmixonboarding.mydomain.ru" \
--env "VIRTUAL_PORT=8080" \
--env "LETSENCRYPT_HOST= jmixonboarding.mydomain.ru" registry.mydomain.ru:5000/ jmixonboarding:0.0.1-SNAPSHOT
Теперь, чтобы все наши контейнеры стартовали автоматически, выполним для каждого команду добавления политики перезапуска:
docker update --restart unless-stopped nginx-proxy
docker update --restart unless-stopped nginx-proxy-acme
docker update --restart unless-stopped testnginx
Таким образом, нам удалось запустить минимальное приложение на Jmix, работающее по HTTPS как локально у разработчика, так и на продуктивном сервере.