Привет, Хабр! На связи Алексей Ежков из из Cloud4Y. Один внешний IPv4, десятки пользователей Exchange и растущий трафик портала — звучит как головоломка? В этой статье я покажу, как мы решили её, заведя всё хозяйство за единственным IP и обеспечив максимальную защиту.

Допустим, в компании «Фирмачка» (доменное имя — firmachka.pro) количество пользователей Exchange Server 2019 перевалило за сотню, а трафик к двум фронтендам корпоративного сайта вырос после маркетинговой кампании. У провайдера — только один публичный IPv4.


Требуется:

  1. Один внешний IP-адрес.

  2. SNI-маршрутизация нескольких DNS-имён.

  3. SSL-offload (TLS 1.2 / 1.3).

  4. Защита от DoS, health-checks, отказоустойчивость для DAG-кластера Exchange и двух веб-узлов.

DNS-записи и сама DAG-инфраструктура настроены корректно и в данной статье не обсуждаются.

Перед началом убедитесь, что:

  • Все необходимые DNS-записи (A-записи для mail, autodiscover, portal, lb) указывают на публичный IP-адрес HAProxy.

  • Порты 25, 80, 443 открыты на файрволе до HAProxy.

  • Exchange DAG и веб-портал настроены и работают корректно во внутренней сети.

  • У вас есть sudo-доступ к серверу с HAProxy.

2. Топология и окружение

  • HAProxy ≥ 2.4 из backports/debian-bookworm.

  • Exchange Server 2019 CU13 (две ноды DAG).

  • Портал на IIS 10 / ASP.NET Core.

3. Откуда берутся файлы *.pem в /etc/ssl/private

3.1. Получаем wildcard-сертификат

Сертификат нужен один — *.firmachka.pro, чтобы покрыть mail, autodiscover, portal, lb и любые будущие имена.

ACME-клиент выпускает wildcard через DNS-01-валидацию:

DOMAIN="firmachka.pro"
CERT_DIR="/etc/ssl/private"

certbot certonly \
        --agree-tos \
        --manual \
        --preferred-challenges dns \
        -d "*.${DOMAIN}" -d "${DOMAIN}" \
        --manual-public-ip-logging-ok

# склейка в формат HAProxy
cat /etc/letsencrypt/live/${DOMAIN}/privkey.pem \
    /etc/letsencrypt/live/${DOMAIN}/fullchain.pem \
    > ${CERT_DIR}/wildcard.${DOMAIN}.pem
chmod 600 ${CERT_DIR}/wildcard.${DOMAIN}.pem

Для автоматического продления wildcard-сертификатов через DNS-01 челлендж обычно используются DNS-плагины Certbot

3.2. DH-параметры

openssl dhparam -out /etc/haproxy/certs/dhparam.pem 2048

Файл подключается директивой ssl-dh-param-file. Без собственных DH-параметров часть сканеров ругается на «weak prime reuse».

После автоматического продления сертификата выполняйте systemctl reload haproxy, иначе HAProxy продолжит отдавать старую цепочку

4. Полный haproxy.cfg

4.1. global

global
    log /dev/log    local0 notice
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    maxconn 20000

    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets prefer-client-ciphers
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
    ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384

    ssl-dh-param-file /etc/haproxy/certs/dhparam.pem
    tune.ssl.default-dh-param 2048

Пояснения построчно:

  • log /dev/log local0 notice — журналируем в rsyslog; уровень notice экономит диск.

  • stats socket — пригодится для Runtime API и атомарной перезагрузки.

  • maxconn 20000 — лимит по памяти: примерно 50 КБ/conn ⇒ ≈1 ГБ RSS.

  • ssl-default-bind-options — отключаем TLS 1.0/1.1, запрещаем session tickets (защита от атак resumption-tracking), навязываем клиенту наш порядок шифров.

  • ssl-default-bind-ciphersuites — suite-ы для TLS 1.3.

  • tune.ssl.default-dh-param — fallback-значение, если ssl-dh-param-file недоступен.

На заметку
Частая ошибка — оставить ssl-default-bind-options без prefer-client-ciphers; тогда старые браузеры выберут RC4.

4.2. defaults

defaults
    mode    http
    log     global
    option  httplog
    option  dontlognull
    option  forwardfor
    option  redispatch
    option  http-server-close
    retries 3
    
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    timeout connect 10s
    timeout client  300s
    timeout server  300s
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 503 /etc/haproxy/errors/503.http

Ключевые моменты:

  • option http-server-close — клиенту keep-alive, а на внутренние сервера соединение закрываем после ответа; экономия на TIME_WAIT.

  • forwardfor + set-header X-Forwarded-Proto — OWA корректно формирует ссылки вида https://.

  • Таймауты 300 s подобраны под длительные загрузки вложений в OWA.

На заметку
Забытый option httpclose ведёт к обрыву крупных загрузок через OWA/OWA Light.

4.3. frontend ft_http — порт 80

frontend ft_http
    bind *:80
    mode http

    acl host_portal hdr(host) -i portal.firmachka.pro
    use_backend be_portal if host_portal

    redirect scheme https code 301 if !host_portal

Разбираем:

  1. Портал доступен и по HTTP :80 (SEO-требование).

  2. Всё остальное принудительно уходит на HTTPS (301).

4.4. frontend ft_https — порт 443

frontend ft_https
    bind *:443 ssl crt /etc/ssl/private/wildcard.firmachka.pro.pem
    mode http

    http-response set-header X-Frame-Options SAMEORIGIN
    http-response set-header X-Content-Type-Options nosniff
    http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains"

    # Защита от DoS
    stick-table type ip size 200k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny status 429 if { sc_http_req_rate(0) gt 100 }

    # ---------- ACL ----------
    acl host_portal        hdr(host) -i portal.firmachka.pro
    acl host_autodiscover  hdr(host) -i autodiscover.firmachka.pro

    acl url_ecp path_beg -i /ecp
     # !!! ВАЖНО: Укажите вашу административную подсеть !!!
    acl admin_network src 192.168.20.0/24
    http-request deny if url_ecp !admin_network

    # ---------- Маршрутизация ----------
    use_backend be_portal_https      if host_portal
    use_backend be_exchange_unified  if host_autodiscover

    default_backend be_exchange_unified

Пояснения:

  • Один wildcard-сертификат обслуживает все имена.

  • autodiscover.firmachka.pro направляется в тот же backend, что и mail.

  • Security-заголовки убирают mixed-content и click-jacking.

  • stick-table фильтрует > 100 запросов/10 s ⇒ HTTP 429 Too Many Requests.

  • Защита ECP: доступ только из админской подсети. Exchange не умеет ограничивать сам.

На заметку
• Таблица 200 k на 30 s ≈ 14 MB RAM. Статус в Runtime API: show table ft_https.
• Если нужен IPv6, заведите отдельную stick-таблицу type ipv6.

4.5. frontend ft_smtp — порт 25

frontend ft_smtp
    bind *:25
    mode tcp
    
    # Ограничение скорости подключений
    stick-table type ip size 50k expire 1m store conn_rate(10s)
    tcp-request connection track-sc0 src
    tcp-request connection reject if { sc_conn_rate(0) gt 10 }
    
    default_backend be_smtp
  • Layer 4 proxy, никаких STARTTLS-offload; Exchange сам отдаёт сертификат.

  • 10 conns / 10 s → reject (борьба с ботнетами, не влияя на легитимный MX-трафик).

На заметку
Серые листы (greylisting) любят быстро пересоединяться. Подберите лимит под вашу почтовую политику.

4.6. backend be_portal

backend be_portal
    mode http
    balance roundrobin
    option httpchk GET /
    http-check expect status 200
    server portal01 192.168.20.10:80  check inter 3s rise 2 fall 3
    server portal02 192.168.20.11:80  check inter 3s rise 2 fall 3
  • rise 2 / fall 3 — узел объявляется «здоровым» после двух успешных чеков и «больным» после трёх неудач.

  • Схема round-robin подходит: сессии портала хранятся в Redis-кластер, нет state-stickiness.

4.7. backend be_portal_https

backend be_portal_https
    mode http
    balance roundrobin
    option httpchk GET /
    http-check expect status 200
    server portal01 192.168.20.10:443 ssl verify none check inter 3s rise 2 fall 3
    server portal02 192.168.20.11:443 ssl verify none check inter 3s rise 2 fall 3
  • Внутренний трафик также шифруется; verify none допустим, так как это изолированная VLAN.

  • Если у портала собственный internal-CA, замените на verify required ca-file /etc/ssl/certs/ca_portal.pem.

4.8. backend be_exchange_unified

backend be_exchange_unified
    mode http
    balance source
    option httpchk GET /owa/healthcheck.htm
    http-check expect status 200
    server ex01 192.168.0.100:443 ssl verify none check inter 3s rise 2 fall 3
    server ex02 192.168.0.101:443 ssl verify none check inter 3s rise 2 fall 3

Почему balance source?

Exchange клеит сессию OWA к конкретному ClientAccessServer. Sticky-cookie тоже решит задачу, но source-hash проще и не ломает ActiveSync с legacy-устройств.

healthcheck.htm — официальный endpoint; возвращает «200 OK Healthy» без аутентификации.

На заметку
После CU-обновления Microsoft иногда меняет ответ с «Healthy» на «OK». Ловите 503? Сравните тело ответа в curl -k.

4.10. backend be_smtp

backend be_smtp
    mode tcp
    balance roundrobin
    option smtpchk EHLO firmachka.pro
    server ex01 192.168.0.100:25 check
    server ex02 192.168.0.101:25 check

option smtpchk посылает EHLO и ждёт кода 250. Это держит порт 25 в «green» даже при частичном падении Exchange.

4.11. listen stats

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats realm "HAProxy Statistics"
    stats auth admin:ChangeMeNow!
    stats refresh 30s
  • Веб-панель по HTTP (не HTTPS). Ограничьте внешним файрволом!

  • stats socket из блока global позволяет вместо веб-GUI пользоваться socat /run/haproxy/admin.sock - и собирать метрики в Prometheus-exporter.


5. Безопасность и производительность

  1. TLS 1.3 + строгий список шифров, SessionTickets отключены.

  2. Один wildcard-сертификат упрощает сопровождение и закрывает все поддомены.

  3. stick-table фильтрует 100 req/10 s или 10 conn/10 s на SMTP.

  4. maxconn 20000 + Linux 5.10 (SO_REUSEPORT) дают 5–6 Гбит/с без CPU-узких мест. Опция SO_REUSEPORT позволяет HAProxy эффективнее использовать несколько ядер CPU, распределяя входящие соединения между несколькими рабочими процессами (worker processes) HAProxy, если они запущены.

  5. Health-checks раз в 3 s обеспечивают SLA ≥ 99.9 % без избыточного сетевого шума.


6. Тестирование

haproxy -c -f /etc/haproxy/haproxy.cfg          # проверка синтаксиса
curl -k https://lb.firmachka.pro/owa/healthcheck.htm  # Проверяем доступность и работоспособность Health Check страницы Exchange OWA
curl -k https://autodiscover.firmachka.pro/autodiscover/autodiscover.xml -H "Content-Type:text/xml"  # Имитируем запрос к службе Autodiscover Exchange через балансировщик
swaks --to recipient@example.com --from sender@firmachka.pro --server mail.firmachka.pro --port 25 -ehlo test  # Тестирование работы SMTP-сервера
openssl s_client -connect lb.firmachka.pro:443 -servername portal.firmachka.pro \    # Получаем информацию о выбранной версии TLS и наборе шифров
        -tlsextdebug -status | grep -E 'Selected|TLSv'

На заметку
Если openssl сообщает New, TLSv1.2, а не TLSv1.3, проверьте версию OpenSSL на клиенте.

7. Эксплуатация

  • Горячий reload: haproxy -f /etc/haproxy/haproxy.cfg -c && systemctl reload haproxy.

  • Runtime-API: echo "show table ft_https" | socat stdio /run/haproxy/admin.sock.

  • Мониторинг: github.com/prometheus/haproxy_exporter читает stats socket.


8. Частые проблемы

Симптом

Причина

Решение

503 на OWA после продления wildcard-серта

забыли reload

systemctl reload haproxy

401/403 при входе в /ecp

ACL admin_network не совпадает с новой VPN-подсетью

скорректировать CIDR

Серый список SMTP режет доставку

stick-table слишком строг

увеличить лимит conn_rate


9. Итог

Мы получили:

  • Wildcard-сертификат *.firmachka.pro, покрывающий все сервисы.

  • SNI-маршрутизацию четырёх DNS-имён (mailautodiscoverportallb) на одном IP.

  • SSL-offload + TLS 1.3 без компромиссов в безопасности.

  • Отказоустойчивые Exchange 2019 DAG и веб-кластер за одним фронтом.

  • Базовую DoS-защиту и удобный мониторинг без лишнего железа.

Официальная документация для углубления:

Комментарии (3)


  1. Johan_Palych
    01.08.2025 07:53

    1. igrblkv
      01.08.2025 07:53

      Там-же SE на замену есть? Он прям поверх 2019-го ставится.


      1. Johan_Palych
        01.08.2025 07:53

        В курсе:
        (Update 7/15/2025: Added a mention of Exchange 2016/2019 ESU program)Upgrading your organization from current versions to Exchange Server SE
        Our final CU for Exchange Server 2019. Code parity with Exchange Server SE RTM (except for any SUs or HUs released before Exchange SE RTM):
        Feb 10, 2025 Released: 2025 H1 Cumulative Update for Exchange Server
        "Today, we are announcing the availability of the 2025 H1 Cumulative Update (CU) for Exchange Server 2019 (aka CU15). This is the last CU we will release for Exchange Server 2019."