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

Допустим, в компании «Фирмачка» (доменное имя — firmachka.pro) количество пользователей Exchange Server 2019 перевалило за сотню, а трафик к двум фронтендам корпоративного сайта вырос после маркетинговой кампании. У провайдера — только один публичный IPv4.
Требуется:
Один внешний IP-адрес.
SNI-маршрутизация нескольких DNS-имён.
SSL-offload (TLS 1.2 / 1.3).
Защита от 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
Разбираем:
Портал доступен и по HTTP :80 (SEO-требование).
Всё остальное принудительно уходит на 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. Безопасность и производительность
TLS 1.3 + строгий список шифров, SessionTickets отключены.
Один wildcard-сертификат упрощает сопровождение и закрывает все поддомены.
stick-table фильтрует 100 req/10 s или 10 conn/10 s на SMTP.
maxconn 20000
+ Linux 5.10 (SO_REUSEPORT) дают 5–6 Гбит/с без CPU-узких мест. Опция SO_REUSEPORT позволяет HAProxy эффективнее использовать несколько ядер CPU, распределяя входящие соединения между несколькими рабочими процессами (worker processes) HAProxy, если они запущены.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-серта |
забыли |
|
401/403 при входе в /ecp |
ACL |
скорректировать CIDR |
Серый список SMTP режет доставку |
stick-table слишком строг |
увеличить лимит conn_rate |
9. Итог
Мы получили:
Wildcard-сертификат
*.
firmachka.pro
, покрывающий все сервисы.SNI-маршрутизацию четырёх DNS-имён (
mail
,autodiscover
,portal
,lb
) на одном IP.SSL-offload + TLS 1.3 без компромиссов в безопасности.
Отказоустойчивые Exchange 2019 DAG и веб-кластер за одним фронтом.
Базовую DoS-защиту и удобный мониторинг без лишнего железа.
Официальная документация для углубления:
«Load Balancing in Exchange Server»: https://learn.microsoft.com/en-us/exchange/architecture/client-access/load-balancing
Руководство по security-headers: https://owasp.org/www-project-secure-headers/
Johan_Palych
Exchange Server 2019 lifecycle
igrblkv
Там-же SE на замену есть? Он прям поверх 2019-го ставится.
Johan_Palych
В курсе:
(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."