Рецепт сервера обычной компании:
- Белый IP
- Веб-сервер (nginx, apache, IIS, winow и т.д.)
- Сертификаты для протокола https
Если есть много денег, то купить ssl сертификаты, белый IP и группу сисадминов для поддержки - это не проблема. Но если ты хочешь создать пет проект в вебе, или создать свой сервис, платить деньги - не вариант. А значит, некоторые ингредиенты рецепта надо заменить на бесплатные альтернативы
Рецепт бесплатного сервера:
- DynamicDNS
- Веб-сервер
- Сертификаты Let's Encrypt
- Docker, потому что пора бы наконец научиться пользоваться.
- Какой-никакой роутер с root доступом и возможностью проброса портов
Итак, начнем по порядку.
1. Dynamic DNS
Динамический DNS (далее по тексту DDNS) нужен для того, чтобы вы могли стучаться в свою локальную сеть. Как это работает? Вам дается адрес, чаще всего состоящий из ВашСубДомен.ДоменПоставщика.com/org или любое другое расширение. При отправке запроса на этот адрес, вызов перенаправляется в вашу локальную сеть. Для того, чтобы DDNS знал, куда пересылать запрос, вы периодически обновляете информацию о вашем текущем IP адресе на хосте DDNS. Для каждого DDNS есть своя инструкция по обновлению информации об IP адресе.
В рамках этой статьи будем использовать вот этот ресурс, потому что он бесплатный, и потому что он элементарно прост в настройках, а также потому что я для этого сервиса нашел все необходимые библиотеки.
Нам нужно перейти на главную страницу, в верхней правой части авторизоваться любым удобным способом и увидеть на личной странице свой уникальный токен
Далее, ниже создать свой домен, увидеть как он появится в списке личных доменов, проверить что там отображается ваш текущий IP адрес. Проверить свой IP адрес можно с помощью этого ресурса.
Запомним токен и домен, далее эти данные нам понадобятся.
2. Веб-сервер
Я думаю для 1Сников не надо объяснять что такое веб-сервер. Практически все так или иначе слышали слова apache и IIS. Но на всякий случай, веб-сервер - это такая хитрая штука, которая умеет принимать http-запросы и выдавать http-ответы. Но! Это важно. В нашем случае мы не будем настраивать веб-сервер в привычном для этой фразы понимании. Дело в том, что нам нужен обратный прокси-сервер. Это такая штука, которая принимает http-запросы и пересылает их дальше по требованию.
Почему именно так?
Потому что мы будем настраивать возможность обмена информацией через защищенный протокол https. Наш прокси должен получить https-запрос, дешифровать его в незащищенный http-запрос и передать его на нужный порт в нашей локальной сети. Далее, из нашей локальной сети от принимает незащищенный http-ответ, шифрует его в защищенный https-ответ и отдает клиенту. Такая "архитектура" позволит нам не заморачиваться с сертификатами в наших сервисах, а отдать все действия по шифровке/дешифровке обратному прокси-серверу.
Обратный прокси-сервер мы будем поднимать в докер контейнере, потому что он нам в локальной сети вообще никуда не упал. Это отдельный сервис, пусть живет отдельной жизнью. Поскольку это будет не единственный контейнер, мы будем использовать docker-compose.
Да, кстати, для обратного прокси-сервера мы будем использовать nginx, потому что пора узнать что есть что-то кроме apache и IIS.
Пример конфигурации контейнера в docker-compose файле:
version: '3'
services:
nginx:
container_name: nginx
image: nginx:stable-alpine3.17-slim
ports:
- 8080:80
- 8443:443
volumes:
- ./nginx:/etc/nginx
- ./letsencrypt:/etc/letsencrypt
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
extra_hosts:
- "host.docker.internal:host-gateway"
Что означают все эти буквы?
"version: '3'" - объявляем, какую версию compose файла мы используем
"services:" - внутри мы будем описывать, какие сервисы мы будем использовать
"nginx:" - имя сервиса (придумываем сами)
"container_name: nginx" - имя контейнера (придумываем сами)
"image: nginx:stable-alpine3.17-slim" - указываем, какой docker образ мы будем использовать в сервисе. Все образы можно посмотреть в docker hub
"ports:
- 8080:80
- 8443:443" - проброс портов в формате "порт локального хоста : порт контейнера". В данном случае контейнер будет слушать порт 8080 и 8443 вашего хоста и передавать на порт 80 и 433 соответственно внутренней сети контейнера. Порт контейнера нужно устанавливать такой же, который прописан в конфигурационном файле нашего прокси-сервера (доберемся и до него в порядке живой очереди). Порт хоста нужно задать такой же, как в настройках проброса портов в роутере
Например: у меня роутер настроен так, все входящее на порт 80 он пробрасывает на порт 8080 моего компьютера, а входящие сообщения на порт 443 отправляет на порт 8443 компьютера.
"volumes:
- ./nginx:/etc/nginx
- ./letsencrypt:/etc/letsencrypt" - монтируем папки в контейнер. То есть папка nginx на вашем компьютере и папка /etc/nginx в контейнере - это одна и та же папка с файлами. Это нужно для того, чтобы мы могли отдавать в контейнер нужные файлы (например конфигурацию или сертификаты) и менять эти файлы в режиме реального времени.
"command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"" - команда, которая выполнится при старте контейнера. Это рекомендованная строка для запуска nginx взятая из гайдов в интернете. И она работает. Все. Идем дальше.
"extra_hosts:
- "host.docker.internal:host-gateway"" - эти строки говорят о том, что мы настраиваем связь между сетью контейнера и хостом. Нужна она для того, чтобы мы могли из докера стучаться в сеть хоста. (далее по тексту будет пример, как это работает)
Пример конфигурации nginx:
events {}
http {
server {
listen 80;
location / {
return 301 https://domain.duckdns.org$request_uri;
}
}
server {
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/domain.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.duckdns.org/privkey.pem;
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
add_header Cache-Control "public, must-revalidate";
add_header Front-End-Https on;
add_header Strict-Transport-Security "max-age=2592000; includeSubdomains";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://host.docker.internal:443;
}
}
}
Тут мы остановимся только на тех строках, которые что-то для нас значат. В остальном - это стандартная конфигурация nginx для обратного прокси-сервера.
"listen 80" - сюда пишем порт, который мы указывали в docker-compose файле, для http-запросов
"return 301 https://domain.duckdns.org$request_uri" - если мы получили незашифрованный http-запрос, мы делаем редирект на https, чтобы получить защищенный https-запрос. Вместо domain нужно указать ваш домен в duckdns, который вы придумали и создали.
"listen 443 ssl http2" - сюда пишем порт, который мы указывали в docker-compose файле, для https-запросов.
"ssl_certificate /etc/letsencrypt/live/domain.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.duckdns.org/privkey.pem;" - путь до сертификатов, которые мы получим позже. Что делать со словом domain, думаю, догадаетесь :)
"ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;" - невероятно важный файлик, без которого наш прокси просто не запустится. Скачать можно с официального репозитория
"proxy_pass http://host.docker.internal:443;" - а вот и отправка расшифрованного сообщения на хост. "host.docker.internal" в нашей конфигурации - это IP адрес хоста (вашего ПК/ноута или с чего вы там все это запускаете). А вот с портом сложнее. Тут надо указать такой порт, который слушает ваш сервис/приложение.
Так должен выглядеть файл с названием nginx.conf.
Ну, вроде с nginx более менее разобрались, по крайней мере в контексте настройки нашего прокси.
3. Сертификаты
Есть такая компания Let's Encrypt. Эта компания поставила перед собой задачу - перевести весь интернет на протокол https. Как это сделать? Правильно. Дать бесплатные сертификаты. Я не работал с платными TLS сертификатами, так как моя сфера работы чуть чуть другая, поэтому вся разница для меня состоит в цене (учитывая что я поднимаю свой домашний сервер).
Далее, для удобного получения сертификатов была написана программа/библиотека на python - Certbot. Теперь получить сертификат можно было командой в командной строке, или вызвав программный интерфейс библиотеки, если вы встроили эту библиотеку в свою программу. Но и это еще не все.
Чтобы получить сертификат, вы должны подтвердить, что защищаемый сертификатами домен принадлежит вам. Для этого вы должны разместить специальную строку в специальном месте, которую центр сертификации обязательно проверит. А поскольку мы используем DDNS, мы не можем просто так куда-то что-то там размещать и проверять. Поэтому, сервисы DDNS создали специальный API, используя который вы можете разместить TXT запись в вашем субдомене DDNS для проверки. А где появляются какие-то API, появляются и энтузиасты, пишущие свои библиотеки/программы для использования этого самого API.
В итоге, чтобы не изобретать велосипед, вы должны перелопатить интернет в поисках той самой библиотеки, у которой будет самая адекватная документация, чтобы не тратить много времени на изучения возможностей. И тут мы приходим к интересному проекту, который был написан для интеграции программы certbot c DDNS DuckDNS - certbot_dns_duckdns. Вот ее мы и будем использовать в нашем проекте.
Пример конфигурации контейнера в docker-compose файле:
version: '3'
services:
certbot:
image: "infinityofspace/certbot_dns_duckdns:latest"
container_name: "certbot"
volumes:
- "./letsencrypt:/etc/letsencrypt"
- "./logs:/var/log/letsencrypt"
command: certonly
--non-interactive
--agree-tos
--email {YOUR_EMAIL}
--preferred-challenges dns
--authenticator dns-duckdns
--dns-duckdns-token {YOUR_TOKEN}
--dns-duckdns-no-txt-restore
--dns-duckdns-propagation-seconds 15
-d "domain.duckdns.org"
-d "*.domain.duckdns.org"
Со значениями полей мы уже знакомы. Тут нас интересует поле "command". Это та самая команда, которая при запуске запустит процесс получения сертификатов. Тут нужно исправить несколько значений. "{YOUR_EMAIL}" - тут надо указать свой почтовый адрес, для регистрации сертификата. "{YOUR_TOKEN}" - сюда нужно установить токен, который вы получили в DuckDNS. Токен нужен как раз для интеграции. Со словами "domain" делать тоже, что и всегда - менять на свой домен из DuckDNS.
Итак, с конфигурациями контейнеров мы ознакомились. Теперь нам надо их совместить:
version: '3'
services:
nginx:
container_name: nginx
image: nginx:stable-alpine3.17-slim
ports:
- 8080:80
- 8443:443
volumes:
- ./nginx:/etc/nginx
- ./letsencrypt:/etc/letsencrypt
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
extra_hosts:
- "host.docker.internal:host-gateway"
certbot:
image: "infinityofspace/certbot_dns_duckdns:latest"
container_name: "certbot"
volumes:
- "./letsencrypt:/etc/letsencrypt"
- "./logs:/var/log/letsencrypt"
command: certonly
--non-interactive
--agree-tos
--email {YOUR_EMAIL}
--preferred-challenges dns
--authenticator dns-duckdns
--dns-duckdns-token {YOUR_TOKEN}
--dns-duckdns-no-txt-restore
--dns-duckdns-propagation-seconds 15
-d "domain.duckdns.org"
-d "*.domain.duckdns.org"
Вот так должен выглядеть файл, с необычным названием docker-compose.yml для запуска (конечно же с вашими правками, которые мы рассмотрели выше).
Структура проекта при этом будет выглядеть примерно так:
- letsencrypt - директория
-- ssl-dhparams.pem - файл из п.2
- nginx - директория
-- nginx.conf - файл конфигурации из п.2
- docker-compose.yml - файл вместивший в себя описание контейнеров
Директории будут монтированы в контейнеры для обмена информации
4. Запуск
Сначала, надо установить docker и docker-compose.
Открыть командную строку и перейти в корневую директорию проекта.
Выполнить команду "docker compose -f "docker-compose.yml" up -d --build"
При первом запуске контейнер с прокси-сервером выдаст ошибку, так как не сможет найти сертификаты, они еще не созданы.
Контейнер certbot предпримет попытку создания сертификатов. Возможно, по какой-то причине не пройдет проверка домена. При этом в контейнере выйдет ошибка
Certbot failed to authenticate some domains (authenticator: dns-duckdns). The Certificate Authority reported these problems:
Domain: domain.duckdns.org
Type: unauthorized
Detail: Incorrect TXT record "{какой-то ключ}" found at _acme-challenge.domain.duckdns.org
Надо проверить, все ли вы правильно указали, соответствует ли ваш IP адрес в 2IP и DuckDNS, и запустить контейнер еще раз.
То, что сертификаты были правильно созданы, будет сопровождаться сообщением в логе:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/domain.duckdns.org/fullchain.pem
Key is saved at: /etc/letsencrypt/live/domain.duckdns.org/privkey.pem
This certificate expires on {какая-то дата окончания}.
При получении такого сообщения, можно запускать контейнер с прокси-сервером. Все готово для работы.
При отправке HTTPS-запроса на адрес или http://domain.duckdns.org, прокси получит данные, расшифрует и отправит HTTP-запрос на ваш хост.
Запускам наш сервис, запускаем контейнер nginx, делаем запрос на адрес https://domain.duckdns.org, получаем результат
Браузер не ругается на небезопасное соединение, сервис отдает ответ, все работает как и задумывалось.
Итог
В статье я постарался аккумулировать результат личного изучения темы. Возможно я где-то допустил ошибку или налил много воды. В любом случае, я надеюсь, кому-то это сэкономит время при настройке домашнего сервера. Весь описанный проект вы можете скачать на гитхаб в моем репозитории, там же есть краткая инструкция по настройке.
Также, вы должны понимать, что вам понадобится еще несколько настроек. Вам надо отправлять в duckdns свой текущий IP адрес для автоматического обновления. Как это сделать описано тут, ну или вы можете отправлять https-запрос типа https://www.duckdns.org/update?domains={YOURVALUE}&token={YOURVALUE}[&ip={YOURVALUE}]
Также, было бы неплохо настроить контейнер certbot на автоматический перевыпуск сертификатов после окончания их действия (срок жизни сертификата - 90 дней). В противном случае раз в 90 дней вы должны будете руками запускать контейнер для перевыпуска сертификатов.