Пришло время добавить поддержку защищённых каналов связи. Главным образом потому, что я планирую «поиграться» с аутентификацией и авторизацией, в том числе и через сторонние сервисы, по протоколу OAuth и OpenID Connect используя Identity Server 4. Да и блогу не помешает — поисковики охотнее сотрудничают. Но прежде чем перейти к практическим занятиям кратко освежим теорию.
Чтобы сделать канал защищённым данные нужно зашифровать симметричным алгоритмом. Для этого клиент и сервер должны использовать один и тот же ключ шифрования — сеансовый ключ. Чтобы клиент мог передать серверу сеансовый ключ по пока ещё не защищённому каналу связи применяют асимметричные алгоритмы шифрования, т.е. сервер предоставляет клиенту открытый ключ для шифровки сообщения, которое затем можно расшифровать только закрытым ключом сервера. Звучит складно, но эта схема уязвима для атаки посредника (человек посредине). В качестве защиты предлагается в схему добавить третью сторону — центр сертификации, который берёт на себя обязанности по выпуску открытого и закрытого ключа для сервера. Фокус в том, что открытый ключ клиенту передаётся в составе сертификата открытого ключа, содержащего также и информацию о центре сертификации с цифровой подписью, сроком действия и информацией о владельце. Таким образом клиент получив сертификат может проверить через центр сертификации, что открытый ключ действительно выдан и принадлежит серверу. Теперь всё хорошо и надёжно ровно до тех пор, пока центр сертификации будет вести себя честно, а не как удостоверяющие центры в России фальсифицировать электронные подписи для перевода накоплений из ПФР в НПФ.
Стало быть задача состоит в получении сертификата у центра сертификации. Существует множество центров по выдаче платных сертификатов и небольшое кол-во по выдаче бесплатных. Среди последних наиболее популярен Let’s encrypt. Ребятки отличились ещё и тем, что создали протокол ACME для автоматизации процесса выдачи, обновления и отзыва криптографических сертификатов. А сторонние разработчики позаботились о создании всевозможных ACME-клиентов, среди которых упоминается и Traefik. Примеров по настройке на просторах интернета полно, но есть один нюанс: большая часть из них в качестве проверки прав на доменное имя использует проверку HTTP-01, в то время как проверка DNS-01 позволяет выпускать сертификаты с подстановкой (wildcard-сертификат), хоть и требует чуть больше телодвижений. И это как раз мой случай потому-то мой регистратор домена Beget отсутствует в списке DNS провайдеров поддерживаемых Traefik. Однако API у Beget есть, а значит интеграция возможна. Поначалу возникла мысль написать консольное приложение на .NET 6.0 и использовать его в качестве провайдера DNS для Traefik, раз уж .NET 6.0 теперь и на Linux’ах работает. Но в ходе обдумывания этой идей вспомнил, что Traefik существует в виде контейнера Docker и соответственно .NET runtime на него поставить будет проблематично… Можно написать простенькое web API приложение с поддержкой Docker’а под Linux и использовать его в качестве DNS провайдера для Traefik. Хоть задача и представляет академический интерес, но мне не хотелось создавать нагрузку на свой сервер там, где этого можно избежать и использовать иное простое и элегантное решение. И я таки его нашёл. Админы наверно сейчас надо мной «поржут», но я не знал, что регистратором домена и провайдером DNS могут быть разные организации. Учитывая это я могу делегировать домен серверам Selectel’а, который любезно предоставляет бесплатный DNS-хостинг и поддерживается в качестве поставщика DNS проверки для Traefik. Технически процесс состоит из следующих шагов:
- прописать DNS сервера Selectel’а для выбранного домена в админке Beget.
- зарегистрироваться в Selectel и добавить требуемый домен.
- добавить для домена запись типа A с указанием соответствующего IP-адреса.
- добавить ещё одну запись типа А для поддоменов используя символ макроподстановки.
- создать новый API ключик в профиле Selectel для последующего использования.
- дождаться смены серверов. Проверить это можно с помощью любого whois-сервиса.
- проверить работу DNS для корневого домена и поддоменов с помощью
nslookup
с указанием DNS сервера Selectel и без него.
Следующий шаг — необходимо внести изменения в статическую и динамическую конфигурацию Traefik. Начнём со статики. Первым делом добавил блок касающийся получения и хранения сертификатов:
certificatesResolvers: myresolver: acme: email: konstantin.ogorodov@gmail.com storage: "/etc/traefik/acme.json" # caServer: https://acme-staging-v02.api.letsencrypt.org/directory dnsChallenge: provider: selectel # SELECTEL_API_TOKEN resolvers: - "188.68.203.5:53" - "77.223.114.10:53" - "188.68.203.10:53" - "77.223.114.5:53"
- Хранилище сертификатов =
"/etc/traefik/acme.json"
потому, что каталогtraefik
образа уже привязан к одноименному каталогу на сервере для получения конфигурационных файлов. - Значение caServer закомментированное в данный момент указывает на тестовый сервер letsencrypt — очень полезно во время настройки, т.к. рабочие сервера имеют ограничение на кол-во запросов в день для пользователя.
- SELECTEL_API_TOKEN необходимо указать в качестве переменной окружения образа Docker согласно документации к поставщикам.
- delayBeforeCheck = 10 на всякий случай, поскольку видел в логах множество неудачных запросов к API Selectel для проверки TXT записи.
- resolvers — указал поскольку знаю какие именно сервера DNS используются для моего домена. Можно не указывать и тогда будут использоваться последние 2 значения из списка.
Далее необходимо добавить точку входа для безопасного https. Сначала я добавил точку только для внешних запросов на порту 8443, а затем решил добавить ещё и для внутренних запросов на стандартном 443-м:
entryPoints: http-internal: address: :80 http-external: address: :8008 http: redirections: entryPoint: to: :443 scheme: https permanent: true https-internal: address: :443 http: tls: certResolver: myresolver domains: - main: "ogorodov.su" sans: - "*.ogorodov.su" https-external: address: :8443 http: tls: certResolver: myresolver domains: - main: "ogorodov.su" sans: - "*.ogorodov.su"
- Все внешние запросы должны работать через безопасное соединение, поэтому точка входа http-external при получении запроса будет его перенаправлять на https путём возврата ответа HTTP 301 Moved Permanently.
- https-external обслуживает внешние запросы на порту 8443. Сертификаты запрашиваются через myresolver добавленный на предыдущем шаге для корневого домена и всех поддоменов благодаря использованию символов макроподстановки. И хорошо бы не забыть настроить на маршрутизаторе перенаправление входящих запросов с 443-го порта на 8443-й сервера.
- https-internal я решил создать специально для WordPress, чтобы он не ругался на отсутствие поддержки https и доступ был на один и тот же порт как внутри сети, так и за её пределами.
- Конфигурация TLS применяемая к обеим точкам доступа согласно документации Traefik точно такая же, как и для отдельных маршрутов. Однако раз уж конфигурация прописана для точки доступа, то все маршруты этой точки унаследуют конфигурацию, если только маршрут не объявит свою собственную.
Далее нужно перезапустить контейнер Traefik и зайти на информационную панель (Dashboard) убедиться что всё работает.
Следующий этап — настройка динамической конфигурации:
http: routers: wordpress-secure: entryPoints: - "https-internal" - "https-external" rule: "Host(`ogorodov.su`)" service: "wordpress-service" wordpress-redirect-to-secure: entryPoints: - "http-internal" rule: "Host(`ogorodov.su`)" service: "wordpress-service" middlewares: - "redirect-to-https" middlewares: redirect-to-https: redirectScheme: scheme: https permanent: true
- Добавил новый маршрут
wordpress-secure
для внутренней и внешней https точек входа. - переименовал маршрут http wordpress для внутренней точки доступа
http-internal
и добавил использование промежуточного обработчика. - добавил промежуточный обработчик перенаправления с http на https.
На всякий случай отмечу, что «танцы с бубном» касательно перевода WordPress для внутренний сети на https ещё и потому, что браузеры любят кэшировать некоторые ответы и в частности запоминают ответ 301 о перенаправлении. Нужно это иметь ввиду во время тестирования и желательно кэширование отключить. Для FF это делается путём запуска инструментов разработчика по F12 -> F1 -> Отключить HTTP-кэш (когда открыта панель инструментов).