Exchange 2019/SE + HAProxy + Let’s Encrypt + Windows CA

Задача

Клиенты из WAN и LAN подключаются к Exchange Server через HAProxy, где внешний TLS-терминирование выполняется сертификатом Let’s Encrypt. Внутреннее соединение между HAProxy и Exchange осуществляется по TLS с взаимной аутентификацией, где доверие обеспечивается сертификатом, выпущенным внутренним Windows Certification Authority (AD CS).

ЧАСТЬ 1. Готовим сервер HAProxy

Шаг 1. Обновляемся и ставим нужные пакеты

Bash
sudo apt update && apt upgrade -y
sudo apt install -y haproxy socat curl git openssl

socat нужен для hot-update через socket. HAProxy использует Runtime API / stats socket, а acme.sh хранит сертификаты и cron-задачу в своём —home.

Шаг 2. Создаём пользователя acme

Bash
sudo adduser \
  --system \
  --home /var/lib/acme \
  --group acme

Шаг 3. Создаем структуру каталогов

Bash
sudo mkdir -p /var/lib/acme/.acme.sh
sudo mkdir -p /etc/haproxy/certs
sudo mkdir -p /etc/haproxy/ca

sudo chown -R acme:acme /var/lib/acme
sudo chmod 750 /var/lib/acme

sudo chown root:haproxy /etc/haproxy/certs  
sudo chmod 2770 /etc/haproxy/certs

sudo chown root:root /etc/haproxy/ca  
sudo chmod 755 /etc/haproxy/ca

Шаг 4. Установка acme.sh

Bash
git clone https://github.com/acmesh-official/acme.sh.git
cd acme.sh

sudo ./acme.sh \
  --install \
  --home /var/lib/acme/.acme.sh \
  --config-home /var/lib/acme/.acme.sh \
  --no-cron \
  --no-profile

sudo ln -s /var/lib/acme/.acme.sh/acme.sh /usr/local/bin/acme.sh
sudo chown -R acme:acme /var/lib/acme/.acme.sh

acme.sh по своей модели хранит сертификаты и конфигурацию в —home; именно этот каталог потом должен использоваться в —issue, —deploy и —cron.

Шаг 5. Регистрация ACME account

Bash
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
acme.sh --home /var/lib/acme/.acme.sh \
  --register-account \
  --server letsencrypt \
  -m youremail@doamin.ru

Шаг 6. Получаем ACCOUNT_THUMBPRINT

Bash
sudo -u acme cat /var/lib/acme/.acme.sh/account.conf

Ищём:

Bash
ACCOUNT_THUMBPRINT='xxxx'

Этот thumbprint нужен HAProxy для stateless challenge. HAProxy в своём официальном гайде использует именно такой шаблон ответа для /.well-known/acme-challenge/….

Шаг 7. Первичная конфигурация HAProxy для bootstrap

На первом проходе не надо включать bind :443 ssl crt /etc/haproxy/certs/mail.domain.ru.pem, потому что PEM ещё не существует.
Сначала делаем bootstrap-конфиг только для 80/tcp.

Пример минимального фрагмента:

Bash
global
    log /dev/log local0
    log /dev/log local1 notice
    stats socket /run/haproxy/admin.sock mode 660 level admin
    maxconn 10000
    tune.ssl.default-dh-param 2048
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    ssl-default-bind-curves X25519:prime256v1:secp384r1
    setenv ACCOUNT_THUMBPRINT 'ТВОЙ_ACCOUNT_THUMBPRINT'

defaults
    log global
    mode http
    option httplog
    timeout connect 10s
    timeout client  30s
    timeout server  30s

frontend fe_bootstrap_http
    bind :80

    http-request return status 200 content-type text/plain \
      lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" \
      if { path_beg '/.well-known/acme-challenge/' }


default_backend be_exchange

Шаг 8. Перезапускаем HAProxy (с проверкой конфига):

Bash
sudo haproxy -c -f /etc/haproxy/haproxy.cfg  
sudo systemctl restart haproxy

Шаг 9. Проверяем challenge:

Bash
curl http://mail.domain.ru/.well-known/acme-challenge/test123  
curl http://autodiscover.domain.ru/.well-known/acme-challenge/test123

Ожидаем ответ:

Bash
test123.<ACCOUNT_THUMBPRINT>

Шаг 10. Первичный выпуск Let’s Encrypt сертификата

Bash
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
acme.sh --home /var/lib/acme/.acme.sh \
  --issue \
  -d mail.domain.ru \
  -d autodiscover.domain.ru \
  --stateless \
  --server letsencrypt

Проверка state:

Bash
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
acme.sh --home /var/lib/acme/.acme.sh --list

ls -la /var/lib/acme/.acme.sh/mail.domain.ru_ecc/

Там должны быть как минимум:

Bash
- mail.domain.ru.cer
- mail.domain.ru.key
- ca.cer
- fullchain.cer

Шаг 11. Подготовка hot-update через socket

Добавляем acme в группу haproxy:

Bash
sudo usermod -aG haproxy acme

Проверяем socket после старта HAProxy:

Bash
ls -l /run/haproxy/admin.sock

Для hot-update через deploy hook у HAProxy и acme.sh нужен доступ к stats socket.
Проверяем доступ socket от имени acme:

Bash
sudo -u acme socat - UNIX-CONNECT:/run/haproxy/admin.sock <<< "show info"

Шаг 12. Deploy в HAProxy

Теперь, когда cert уже выпущен, кладём production PEM:

Bash
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
DEPLOY_HAPROXY_HOT_UPDATE=yes \
DEPLOY_HAPROXY_STATS_SOCKET=UNIX:/run/haproxy/admin.sock \
DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs \
DEPLOY_HAPROXY_PEM_NAME=mail.domain.ru.pem \
acme.sh --home /var/lib/acme/.acme.sh \
  --deploy -d mail.domain.ru --deploy-hook haproxy

Выставляем права:

Bash
sudo chown root:haproxy /etc/haproxy/certs/mail.domain.ru.pem
sudo chmod 660 /etc/haproxy/certs/mail.domain.ru.pem

Проверяем, что deploy hook закрепил параметры:

Bash
grep -i "Le_Deploy\|haproxy" /var/lib/acme/.acme.sh/mail.domain.ru_ecc/mail.domain.ru.conf

Шаг 13. Переключение HAProxy на 443/tls

Теперь редактируем frontend и включаем 443 уже с существующим PEM

Bash
# ==================== FRONTEND HTTP (порт 80) ====================
frontend fe_http
    bind :80

    # ACME challenge (stateless)
    http-request return status 200 content-type text/plain \
      lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" \
      if { path_beg '/.well-known/acme-challenge/' }

    # Всё остальное → HTTPS
    http-request redirect scheme https unless { path_beg '/.well-known/acme-challenge/' }

# ==================== FRONTEND HTTPS (порт 443) ====================
frontend fe_https
    bind :443 ssl crt /etc/haproxy/certs/mail.domain.ru.pem alpn h2,http/1.1

    # ACME challenge тоже нужен на 443
    http-request return status 200 content-type text/plain \
      lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" \
      if { path_beg '/.well-known/acme-challenge/' }

#    default_backend be_exchange

Проверка и запуск:

Bash
sudo haproxy -c -f /etc/haproxy/haproxy.cfg  
sudo systemctl restart haproxy

Шаг 14. Автообновление

Добавляем cron от имени acme:

Bash
sudo crontab -u acme -e

Добавляем:

Bash
0 3 * * * /usr/local/bin/acme.sh --home /var/lib/acme/.acme.sh --cron --deploy-hook haproxy >> /var/log/acme-cron.log 2>&1

Шаг 15. Тест контура ACME

Bash
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
acme.sh --home /var/lib/acme/.acme.sh \
  --cron --force

Потом проверяем внешний сертификат:

Bash
openssl s_client -connect mail.domain.ru:443 -servername mail.domain.ru </dev/null | \
openssl x509 -noout -issuer -dates -fingerprint

Если fingerprint/даты меняются после --cron --force, значит всё работает как положено.

ЧАСТЬ 2 — Exchange 2019 + Windows CA

Шаг 1. Генерация CSR на Exchange

PowerShell
$certDir = 'C:\Cert'  
New-Item -ItemType Directory -Path $certDir -Force | Out-Null  
  
$reqPath = Join-Path $certDir 'mail.domain.ru.req'  
  
$txtrequest = New-ExchangeCertificate `  
-GenerateRequest `  
-PrivateKeyExportable $true `  
-FriendlyName 'mail.domain.ru' `  
-SubjectName 'CN=mail.domain.ru' `  
-DomainName mail.domain.ru,autodiscover.domain.ru  
  
Set-Content -Path $reqPath -Value $txtrequest

Шаг 2. Отправляем запрос в AD CS

PowerShell
$reqPath = 'C:\Cert\mail.domain.ru.req'
$cerPath = 'C:\Cert\mail.domain.ru.cer'

certreq.exe -submit `
  -config 'CA-SERVER\CA-NAME' `
  -attrib 'CertificateTemplate:WebServer' `
  $reqPath `
  $cerPath

CA-SERVER\CA-NAME — доменное имя центра сертификации

Шаг 3. Импортируем в Exchange

PowerShell
Import-ExchangeCertificate -FileData ([System.IO.File]::ReadAllBytes('C:\Cert\mail.domain.ru.cer'))

Шаг 4. Проверяем сертификат и ищем thumbprint

PowerShell
Get-ExchangeCertificate | Format-List FriendlyName,Thumbprint,Subject,CertificateDomains,Services,NotAfter,Status

Проверяем содержимое:

  • Subject содержит CN=mail.domain.ru
  • CertificateDomains содержит:
    • mail.doamin.ru
    • autodiscover.domain.ru
  • Status нормальный
  • NotAfter соответствует сроку нового cert

Шаг 5. Назначаем сертификат сервисам Exchange

PowerShell
Enable-ExchangeCertificate -Thumbprint <THUMBPRINT> -Services IIS,SMTP,IMAP,POP

Шаг 6. Экспорт сертификата Exchange в PFX

PowerShell
$pwd = Read-Host 'Введите пароль для PFX' -AsSecureString  
  
$cert = Export-ExchangeCertificate `  
-Thumbprint <THUMBPRINT> `  
-BinaryEncoded:$true `  
-Password $pwd  
  
[System.IO.File]::WriteAllBytes('C:\Cert\mail.domain.ru.pfx', $cert.FileData)

ЧАСТЬ 3 — HAProxy доверие к Windows CA

План действий:

  • HAProxy снаружи отдаёт Let’s Encrypt;
  • HAProxy внутрь, к Exchange, должен доверять Windows CA;
  • для этого HAProxy нужен root CA в PEM.

Шаг 1. Экспорт root CA на Windows CA сервере

Тут каюсь несмог написать нормальный powershell скрипт для экспорта сертификата. кинул задачу в беклог, будет время напишу протеструю и статью дополню.

Пока только ручно режим через MMC:

  • certlm.msc
  • Trusted Root Certification Authorities
  • выбрать root CA
  • All TasksExport
  • Base-64 encoded X.509 (.CER)

Передаем файл на сервер Haproxy:

PowerShell
scp domain-root-ca.cer user@ip:/home/user/
scp mail.domain.ru.pfx user@ip:/home/user/

Шаг 2. Подготовка root CA и server cert на HAProxy

Конвертируем root CA в PEM:

Если domain-root-ca.cer уже Base64, обычно сработает так:

PowerShell
openssl x509 -in /home/user/domain-root-ca.cer -out /home/user/domain-root-ca.pem

Если это DER, то так:

PowerShell
openssl x509 -in /home/user/domain-root-ca.cer -inform der -out /home/user/domain-root-ca.pem

Кладём root CA в отдельный каталог HAProxy:

PowerShell
sudo cp /home/user/domain-root-ca.pem /etc/haproxy/ca/domain-root-ca.pem
sudo chown root:root /etc/haproxy/ca/domain-root-ca.pem
sudo chmod 644 /etc/haproxy/ca/domain-root-ca.pem

Извлекаем leaf-сертификат Exchange для проверки trust. server-cert.pem — это именно сертификат Exchange (leaf), который нужен только для локальной проверки доверия.

PowerShell
openssl pkcs12 -in /home/manager/mail.domain.ru.pfx -clcerts -nokeys -out /home/user/server-cert.pem

Проверка доверия к внутреннему сертификату

Проверяем, что root CA действительно доверяет серверному cert Exchange:

PowerShell
openssl verify -CAfile /etc/haproxy/ca/domain-root-ca.pem /home/user/server-cert.pem

Ожидаемый ответ:

PowerShell
/home/user/server-cert.pem: OK

Backend в HAProxy

Для чистой схемы лучше использовать явный CA-файл, а не системный bundle:

PowerShell
backend be_exchange
    server exch s-m01-mail-001.domain.loc:443 \
      check inter 10s \
      ssl verify required \
      sni str(mail.domain.ru) \
      verifyhost mail.domain.ru \
      ca-file /etc/haproxy/ca/domain-root-ca.pem

ЧАСТЬ 4 — Чек-лист проверки

Тест 1. Внешний TLS

PowerShell
openssl s_client -connect mail.domain.ru:443 -servername mail.domain.ru </dev/null | \  
openssl x509 -noout -issuer -dates -fingerprint

Ожидаем:

  • issuer = Let’s Encrypt
  • актуальные даты
  • fingerprint меняется после renew

Тест 2. Внутренний TLS Exchange

PowerShell
openssl s_client -connect s-m01-mail-001.domain.loc:443 \  
-servername mail.domain.ru \  
-CAfile /etc/haproxy/ca/domain-root-ca.pem

Ожидаем:

  • issuer = твой Windows CA
  • Verify return code: 0 (ok)

Тест 3. Health backend’ов HAProxy

PowerShell
echo "show servers state" | socat stdio /run/haproxy/admin.sock

Тест 4. ACME state

PowerShell
sudo -u acme env -i \
HOME=/var/lib/acme \
USER=acme \
LOGNAME=acme \
PATH=/usr/local/bin:/usr/bin:/bin \
acme.sh --home /var/lib/acme/.acme.sh --list

Тест 5. Проверка hot-update socket

PowerShell
sudo -u acme socat - UNIX-CONNECT:/run/haproxy/admin.sock <<< "show info"
sudo -u acme socat - UNIX-CONNECT:/run/haproxy/admin.sock <<< "show ssl cert"

Тест 6. Тест полной автоматики

PowerShell
openssl s_client -connect mail.domain.ru:443 -servername mail.domain.ru </dev/null | \
openssl x509 -noout -issuer -dates -fingerprint

Если внешний сертификат меняется — автоматизация закрыта

Подписаться
Уведомить о
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии