Настройка Reverse Proxy для домашнего сервера на FreeBSD и конфигурация сети для сервисов в jails

За последние пару месяцев мне пару раз пришлось настраивать сеть (для моей собственной инфраструктуры и части сервисов проекта Poridge.club), в которой присутствует публичный шлюз и домашний сервер, таким образом, чтобы сделать для пользователя, обращающегося к сервисам на домашнем сервере, видимость того, что сервисы находятся на шлюзе. Рассмотрю последовательность действий на примере собственной сети, т.к. она сложнее из-за наличия jail’ов.

В наличии:

  • Сервер HP Proliant DL380p, стоящий дома (достойно отдельной статьи с фотоотчетом). Канал 300 Мбит/с. Установлена FreeBSD 13.1-RELEASE. Далее - iota.;
  • Арендованная за ~100 руб/мес VPS (0.5 ядра, 500МБ RAM, канал 200 Мбит/с). Установлен Debian GNU/Linux 11. Далее - gateway:

Reverse proxy scheme

Примерно так должна выглядеть наша сеть в итоге. Следует обратить внимание на то, что трафик от домашнего сервера должен проходить обратно через gateway, чтоб из ответов iota нельзя было выяснить домашний IP по source IP.

Необходимость подобной конфигурации обусловлена несколькими факторами. Во-первых, у домашнего сервера отсутствует белый статический IP (находится за NAT’ом), непонятно, к чему привязывать A запись DNS. Во-вторых, я посчитал, что нежелательно выставлять напоказ IP домашнего сервера.

Для начала нам потребуется организовать VPN для iota и gateway, для этого мы воспользуемся Wireguard. Выбор пал на него по нескольким причинам:

  • Достаточно прост в настройке;
  • Имеет высокую скорость передачи по сравнению с альтернативами, т.к. работает в режиме ядра (в отличие от, например, OpenVPN);
  • Небольшая кодовая база и малое количество поддерживаемых алгоритмов (“less is more”) - большую кодовую базу сложнее поддерживать, выше вероятность ошибки, приводящей к уязвимостям.

Настройка Wireguard

Начнем с установки Wireguard на gateway (здесь и далее первая строка в блоках команд - имя системы, на которой данная команда должна быть выполнена, иногда - с указанием редактируемого файла или jail’а):

[gateway]
$ doas apt-get install wireguard

Генерируем приватный ключ и выставляем разрешения на полученный файл (чтение и модификация только для владельца):

[gateway]
$ wg genkey | sudo tee /etc/wireguard/private.key
$ doas chmod go= /etc/wireguard/private.key

Генерируем публичный ключ по полученному приватному:

[gateway]
$ doas cat /etc/wireguard/private.key | wg pubkey | doas tee /etc/wireguard/public.key

Для VPN мы будем использовать подсеть 10.1.0.0/24.

Создадим файл конфигурации Wireguard:

[gateway]
$ doas vim /etc/wg0.conf

Внесем в файл следующие строки:

[gateway:/etc/wireguard/wg0.conf]
[Interface]
Address = 10.1.0.1/24
SaveConfig = true
ListenPort = 51820
PrivateKey = *gateway:/etc/wireguard/private.key*

Где поле PrivateKey должно соответствовать содержанию файла /etc/wireguard/private.key на gateway. Поле SaveConfig отвечает за то, что при выключении Wireguard, изменения в конфигурации, внесенные “на лету”, будут сохранены.

Для того, чтоб мы могли использовать gateway в качестве шлюза, следует включить там IP forwarding:

[gateway]
$ doas vim /etc/sysctl.conf

И добавим в конец файла следующую строку:

[gateaway:/etc/sysctl.conf]
net.ipv4.ip_forward=1

Для загрузки новых параметров:

$ doas sysctl -p

Все, теперь мы можем включить и запустить Wireguard на gateway:

[gateway]
$ doas systemctl enable wg-quick@wg0.service
$ doas systemctl enable wg-quick@wg0.service

Пришло время настроить Wireguard на iota:

[iota]
$ doas pkg install wireguard-tool wireguard-kmod

Сгенерируем приватный ключ и установим права доступа:

[iota]
$ wg genkey | doas tee /usr/local/etc/wireguard/private.key
$ doas chmod go= /usr/local/etc/wireguard/private.key

Создадим соответствующий публичный ключ:

[iota]
$ doas cat /usr/local/etc/wireguard/private.key | wg pubkey | doas tee /usr/local/etc/wireguard/public.key

Узнаем IP gateway:

[gateway]
$ doas  ip route list default
default via 45.89.228.1 dev eth0 onlink

Создадим файл конфигурации Wireguard на iota:

[iota]
$ doas vim /usr/local/etc/wireguard/wg0.conf

И занесем туда следующее:

[Interface]
PrivateKey = *iota:/usr/local/etc/wireguard/private.key*
Table = off
PostUp = route delete default; route add 45.89.228.240 192.168.88.1; route add default 10.1.0.1
PreDown = route delete default; route delete 45.89.228.240; route add default 192.168.88.1
Address = 10.1.0.2/24

[Peer]
PublicKey = *gateway:/etc/wireguard/public.key*
AllowedIPs = 0.0.0.0/0
Endpoint = 45.89.228.240:51820

Здесь следует вставить содержимое двух файлов - /usr/local/etc/wireguard/private.key с iota и /etc/wireguard/public.key с gateway. Настройкой Table = off мы говорим Wireguard не менять таблицы маршрутизации, т.к позже мы это сделаем сами. В поле PostUp находится список команд, которые следует выполнить после успешного запуска Wireguard. В данном случае мы удаляем маршрут по умолчанию, задаем, что следует искать маршрут до публичного адреса gateway через домашний роутер (192.168.88.1) и добавляем новый маршрут по умолчанию - уже через gateway. Перед выключение Wireguard (PreDown) мы возвращаем таблицу маршрутизации в исходное состояние. В Endpoint мы указываем публичный IP адрес gateway (который мы выяснили командой ip route list default выше) и порт, который задали в конфиге Wireguard на gateway. Особое внимание следует уделить полю AllowedIPs. Если установить его в значение 0.0.0.0/0, то будет возможность отправлять трафик с любым destination IP через интерфейс Wireguard.

Добавим iota как пира на gateway.

[gateway]
$ doas wg set wg0 peer *iota:/usr/local/etc/wireguard/public.key* allowed-ips 10.1.0.2

Добавим следующие строки в файл /etc/rc.conf на iota:

[iota:/etc/rc.conf]
...
wireguard_enable="YES"
wireguard_interfaces="wg0"

Теперь можно запустить Wireguard на iota:

[iota]
$ doas service wireguard start wg0

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

[iota]
$ doas wg show
interface: wg0
  public key: 2G6vGhmq57EE3jTLaRtKerunzvJzELqTEi6Db2TPbFk=
  private key: (hidden)
  listening port: 64424

peer: fcQbxRPgI+DgI1wctuRPnQK4aK0FpNHd2j88Ri3fqx4=
  endpoint: 45.89.228.240:51820
  allowed ips: 0.0.0.0/0
  latest handshake: 1 minute, 14 seconds ago
  transfer: 1011.33 MiB received, 759.34 MiB sent

Настройка файрвола на gateway

Следующий важный шаг - настройка файрвола на gateway для проброса необходимых портов и использования gateway в соответствии с его названием. Первым делом следует сделать наш файрвол “нормально закрытым”, так что начнем с цепочки INPUT таблицы filter. Добавляем правила, разрешающие входящие соединения на порты 22 (SSH, TCP) и 51820 (Wireguard, UDP):

[gateway]
$ doas iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
$ doas iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT

Разрешим также входящие соединения для трафика, являющегося ответом на запросы с данной машины (иначе мы не сможем сделать даже apt update):

[gateway]
$ doas iptables -A INPUT -i eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT

И зададим политику DROP по умолчанию входящего трафика:

[gateway]
$ doas iptables -P INPUT DROP

Перейдем к настройке цепочки FORWARD таблицы filter. Разрешим все пакеты, принадлежащие уже установленным входящим соединения, и все исходящие пакеты на интерфейсе wg0:

[gateway]
$ doas iptables -A FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
$ doas iptables -A FORWARD -i wg0 -j ACCEPT

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

[gateway]
$ doas iptables -A FORWARD -i eth0 -o wg0 -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j ACCEPT
$ doas iptables -A FORWARD -i eth0 -o wg0 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

Следует обратить внимание, что данные правила будут применяться только к новым соединениям, пропуск пакетов уже установленных соединений мы указали выше. Установим политику DROP по умолчанию для цепочки FORWARD:

[gateway]
$doas iptables -P FORWARD DROP

Перейдем к заполнению таблицы nat. Настроим DNAT для необходимых портов:

[gateway]
$ doas iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport --dports 80,443 -j DNAT --to-destination 10.1.0.2
$ doas iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8222 -j DNAT --to-destination 10.1.0.2:22

В результате, y любого пакета, пришедшего на порты 80, 443 и 8222 gateway будет изменен destination IP, и пакет продолжит свой путь до iota.

Осталось завершить настройку добавлением SNAT:

[gateway]
$ doas iptables -t nat -A POSTROUTING -o eth0 -s 10.1.0.0/24 -j SNAT --to-source 45.89.228.240

У любого пакета, пришедшего из сети VPN, source IP меняется на публичный IP gateway, что создает для клиента видимость того, что пакет изначально был отправлен с gateway.

Теперь следует сделать так, чтоб заданные нами правила “переживали” перезагрузку системы.

[gateway]
$ doas apt-get install iptables-persistent
$ doas iptables-save > /etc/iptables/rules.v4
$ doas systemctl enable netfilter-persistent.service

Теперь правила будут загружаться при загрузке системы.

Мой файл правил iptables можно посмотреть здесь.

Настройка jail’ов

FreeBSD Jail (англ. jail — «тюрьма») — механизм виртуализации в системе FreeBSD, позволяющий создавать внутри одной операционной системы FreeBSD несколько независимо работающих FreeBSD на том же ядре операционной системы, но совершенно независимо настраиваемых с независимым набором установленных приложений. Wikipedia

Кратко пройдемся по процессу создания и настройки jail’ов и сети в них. Для начала создадим т.н. basejail, который затем сможем клонировать средствами ZFS. Создаем датасет zroot/jails, который будет смонтирован в /usr/local/jails:

[iota]
$ doas zfs create -o mountpoint=/usr/local/jails zroot/jails

Создаем датасет basejail:

[iota]
$ doas zfs create zroot/jails/basejail

Установим систему в basejail и создадим снапшот, который потом будем использовать как основу для jail’ов:

[iota]
$ fetch "http://ftp.freebsd.org/pub/FreeBSD/snapshots/amd64/13.1-STABLE/base.txz"
$ doas tar -xf base.txz -C /usr/local/jails/basejail
$ doas cp /etc/resolv.conf /usr/local/jails/basejail/etc
$ doas freebsd-update -b /usr/local/jails/basejail fetch install
$ zfs snapshot zroot/jails/basejail@13.1-121222

Создадим jail для nginx:

[iota]
$ doas zfs clone zroot/jails/basejail@13.1-121222 zroot/jails/nginx

Jail’у требуется IP адрес для взаимодействия с внешним миром. Для этой цели мы будем использовать подсеть 172.16.1.0/24 Склонируем интерфейс lo0 и зададим псевдоним для него, имеющий адрес 172.16.1.1 (будет адресом jail’а nginx):

[iota:/etc/rc.conf]
cloned_interfaces="lo1"
ifconfig_lo1_alias0="inet 172.16.1.1 netmask 255.255.255.0"

Чтоб не перезагружать систему, сделаем то же самое командами:

[iota]
$ doas ifconfig lo1 create
$ doas ifconfig lo1 alias 172.16.1.1 netmask 255.255.255.0

Пришло время настроить файрвол для проброса портов в получившийся jail. Включим Packet Filter:

[iota:/etc/rc.conf]
pf_enable="YES"

Откроем файл конфигурации pf:

[iota]
$ doas vim /etc/pf.conf

И внесем туда следующие строки:

[iota:/etc/pf.conf]
ext_if = "vtnet0"
ext_addr = $ext_if:0
int_if = "lo1"
jail_net = $int_if:networknat on $ext_if from $jail_net to any -> $ext_addr port 1024:65535 static-port

nginx_addr = "172.16.1.1"
nginx_ports = "{ 80, 443 }"
rdr pass on $ext_if inet proto tcp to port $nginx_ports -> $nginx_addr

Первая половина разрешает исходящие соединения из jail’ов, вторая - настраивает проброс портов 80 и 443 в jail nginx.

Проверим корректность заданных правил:

[iota]
$ doas pfctl -vnf /etc/pf.conf

Если все в порядке, запустим pf:

[iota]
$ doas service pf start

Осталось лишь сконфигурировать jail со nginx’ом. Откроем общий файл настроек для jail’ов:

[iota]
$ doas vim /etc/jail.conf

И впишем туда следующее:

[iota:/etc/jail.conf]
exec.start ="/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.clean;
mount.devfs;path = "/usr/local/jails/$name";

nginx {
   host.hostname = "nekoea.red";
   ip4.addr = 172.16.1.1;
}

Здесь мы задаем выбранный IP из подсети 172.16.1.0/24 для jail’а nginx и задаем имя хоста, которое должно соответствовать домену, на котором крутится наш сервис.

Включим jail:

[iota:/etc/rc.conf]
jail_enable="YES"

И запустим jail nginx:

[iota]
$ doas service jail start nginx

Зайдем в запущенный jail:

[iota]
$ doas jexec nginx sh

Установим и запустим nginx:

[iota(nginx)]
# pkg install nginx
# service nginx onestart

После этого, открыв веб-страницу по адресу nekoea.red, мы увидим приветствие nginx.

Для меня это был относительно длинный путь проб и ошибок, несколько лет назад для подобной конфигурации использовал SSH Port Forwarding со скриптом, который перезапускал SSH, когда туннель отваливался. Затем я осилил связку tinc и Haproxy, у которой тоже были изрядные недостатки, в числе которых - относительно низкая скорость передачи и невозможность проброса UDP трафика. И вот, наконец наевшись кактуса, сделал нормальное решение, которое шустро работает и не разваливается в самый неподходящий момент.

Спасибо за прочтение, надеюсь, что данная статья пригодилась Вам =3 Буду крайне признателен за конструктивную критику и замечания, мои контакты указаны на сайте nekoea.red.

Источники

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License