HAProxy — «проброс» 80 и 443 порта с нескольких серверов за NAT

Введение

У меня один физический сервер с Proxmox, на котором крутится несколько виртуалок и LXC-контейнеров.
Там-же я запускаю и разные тестовые продукты. У меня есть мания что даже тестовую систему хочется сразу вывести на поддомен\домен и прикрутить к ней сертификат Let’s Encrypt.
От сюда появилась задача поднять реверс-прокси, который было бы не сложно настраивать под новое условие, при этом хотелось бы без лишней возни передавать реальный IP клиента. Так же стояла задача чтобы было ограничение доступа по IP, для систем которые пока не готовы быть открыты в мир, но нужны мне с действующим сертификатом.

Для большинства проектов, даже в официальной документации ставится NGINX, сначала я подумал на него, но в итоге покопавшись в сети и вспомнив свой опыт с pfSense, решил остановиться на HAProxy.

⚠️ Важно: конфигурация изложенная ниже не поддерживает wildcard-сертификаты. Каждый сайт должен иметь свой собственный сертификат.

1. Архитектура

  • Proxmox — основная платформа виртуализации.
  • LXC-контейнер с Debian 12 — здесь крутится HAProxy, слушает порты 80 и 443.
  • Backend-сервера (виртуалки) — на них установлены веб-сервера с собственными сертификатами.
  • Все виртуалки находятся в одной внутренней сети, например 192.168.1.0/24.

2. Установка HAProxy

В LXC-контейнере с Debian 12 устанавливаем HAProxy:

sudo apt update
sudo apt install haproxy -y

3. Базовая конфигурация

Создаём или редактируем /etc/haproxy/haproxy.cfg

📄
haproxy.cfg
Копировать
# Глобальные настройки
global
    log /dev/log local0
    stats socket /run/haproxy/admin.sock mode 660 level admin
    daemon
    maxconn 10000
    nbthread 2
    tune.ssl.default-dh-param 2048
        
# Настройки по умолчанию
defaults
    log global
    mode http
    option dontlognull
    timeout connect 10s
    timeout client 5m
    timeout server 5m

# Frontend для HTTPS
# Мы делаем упор на то что у каждой машины свой сертифика и при установки соединения сверяем домен сертификата и запрошеный
frontend HTTPS
    bind 0.0.0.0:443
    mode tcp
    tcp-request inspect-delay 25s
    tcp-request content accept if { req.ssl_hello_type 1 }
    use_backend HTTPS

backend HTTPS
    mode tcp
    option tcp-check

    # ACL для develop-доменов с ограничением по IP
    # в файле allow_list.txt мы храним списко IP с который разрешён вход
    acl safe_ip src -n -f /etc/haproxy/allow_list.txt
    tcp-request content reject if { req.ssl_sni -i site2.ru } !safe_ip
    tcp-request content reject if { req.ssl_sni -i site3.ru } !safe_ip

    # Динамическое сопоставление по имени хоста
    use-server %[req.ssl_sni] if { req.ssl_sni -m found }
        server site1.ru 192.168.1.11:443
        server site2.ru 192.168.1.12:443
        server site3.ru 192.168.1.13:443

# Frontend для HTTP (редирект на HTTPS)
# В отличии от HTTPS мы можем напрямую получать домен из запроса, так мы можем отдавать правильный сервер без каких-либо проблем
frontend HTTP
    bind 0.0.0.0:80
    mode http
    option forwardfor
    http-request redirect scheme https unless { path_beg /.well-known/acme-challenge }
    use_backend HTTP if { path_beg /.well-known/acme-challenge }

backend HTTP
    mode http
    http-request set-var(txn.txnhost) hdr(host)
    
    # Динамическое сопоставление по имени хоста
    use-server %[var(txn.txnhost)] if { var(txn.txnhost) -m found }
        server site1.ru 192.168.1.11:80
        server site2.ru 192.168.1.12:80
        server site3.ru 192.168.1.13:80

На этом этапе HAProxy принимает входящие соединения, распределяет трафик по доменам через SNI и ограничивает доступ к develop-сайтам по IP.

4. Передача реального IP клиента

По умолчанию backend-сайты видят IP самого LXC-контейнера.
Чтобы они получали настоящий адрес клиента, добавляем в секцию backend HTTPS строку: source 0.0.0.0 usesrc clientip

Теперь секция выглядит так:

📄
haproxy.cfg
Копировать
backend HTTPS
    mode tcp
    option tcp-check
    source 0.0.0.0 usesrc clientip

    acl safe_ip src -n -f /etc/haproxy/allow_list.txt
    tcp-request content reject if { req.ssl_sni -i site2.ru } !safe_ip
    tcp-request content reject if { req.ssl_sni -i site3.ru } !safe_ip

    use-server %[req.ssl_sni] if { req.ssl_sni -m found }
        server site1.ru 192.168.1.11:443
        server site2.ru 192.168.1.12:443
        server site3.ru 192.168.1.13:443

5. Сетевые нюансы

При включённом source 0.0.0.0 usesrc clientip HAProxy подставляет IP клиента как исходный адрес.
Чтобы такие пакеты корректно проходили, нужно:

  • чтобы все backend-сервера использовали контейнер с HAProxy в качестве шлюза по умолчанию;
  • чтобы сам контейнер мог маршрутизировать трафик обратно во внешнюю сеть.

⚠️ Иначе ответы не вернутся по правильному пути, и соединения будут рваться.

Также в этом режиме локальные клиенты (из той же подсети) не смогут обращаться к сайтам через HAProxy, потому что подмена исходного IP нарушает локальную маршрутизацию.

6. Настройка iptables и маршрутизации

В LXC-контейнере создаём правила для таблицы mangle, которые обеспечивают обратную маршрутизацию пакетов:

💻
bash
Копировать
sudo iptables -t mangle -N DIVERT
sudo iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
sudo iptables -t mangle -A PREROUTING -p udp -m socket -j DIVERT
sudo iptables -t mangle -A DIVERT -j MARK --set-mark 1
sudo iptables -t mangle -A DIVERT -j ACCEPT
sudo ip rule add fwmark 1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100

Эти команды создают отдельную таблицу маршрутов 100 и направляют туда помеченные пакеты, чтобы ядро разрешало отправку с адреса клиента.

7. Автоматизация маршрутов через systemd

Чтобы после перезагрузки не прописывать маршруты вручную, создаём сервис:

sudo vi /etc/systemd/system/01-static-route.service

📄
01-static-route.service
Копировать
[Unit]
Description=Add route table 100
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=-/usr/sbin/ip route add local 0.0.0.0/0 dev lo table 100
ExecStart=-/usr/sbin/ip rule add fwmark 1 lookup 100

[Install]
WantedBy=multi-user.target

Активируем:

sudo systemctl daemon-reload
sudo systemctl enable —now 01-static-route.service

8. Ограничение доступа к develop-доменам

Файл /etc/haproxy/allow_list.txt содержит IP-адреса, которым разрешён доступ:

192.168.1.0/24
10.0.0.0/8
203.0.113.55

HAProxy проверяет этот список и блокирует все остальные подключения к закрытым сайтам.

9. Итог

Теперь вся схема работает стабильно:

✅ HAProxy в LXC принимает весь трафик снаружи.
✅ Каждый backend-сайт использует свой Let’s Encrypt сертификат.
✅ Реальный IP клиента прокидывается до backend-сервера.
✅ develop-домены доступны только с разрешённых IP.
✅ Контейнер с HAProxy выполняет роль шлюза для внутренних серверов.

Если отключить source 0.0.0.0 usesrc clientip, то iptables и шлюз можно не настраивать —
но backend-сайты будут видеть IP контейнера, а не клиента.

Обновлено 06.11.2025
comments

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *