Задача

Клиенты из WAN и LAN подключаются к Exchange Server через HAProxy, где внешний TLS-терминирование выполняется сертификатом Let’s Encrypt. Внутреннее соединение между HAProxy и Exchange осуществляется по TLS с взаимной аутентификацией, где доверие обеспечивается сертификатом, выпущенным внутренним Windows Certification Authority (AD CS).
ЧАСТЬ 1. Готовим сервер HAProxy
Шаг 1. Обновляемся и ставим нужные пакеты
sudo apt update && apt upgrade -y
sudo apt install -y haproxy socat curl git opensslsocat нужен для hot-update через socket. HAProxy использует Runtime API / stats socket, а acme.sh хранит сертификаты и cron-задачу в своём —home.
Шаг 2. Создаём пользователя acme
sudo adduser \
--system \
--home /var/lib/acme \
--group acmeШаг 3. Создаем структуру каталогов
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
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.shacme.sh по своей модели хранит сертификаты и конфигурацию в —home; именно этот каталог потом должен использоваться в —issue, —deploy и —cron.
Шаг 5. Регистрация ACME account
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
sudo -u acme cat /var/lib/acme/.acme.sh/account.confИщём:
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.
Пример минимального фрагмента:
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 (с проверкой конфига):
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxyШаг 9. Проверяем challenge:
curl http://mail.domain.ru/.well-known/acme-challenge/test123
curl http://autodiscover.domain.ru/.well-known/acme-challenge/test123Ожидаем ответ:
test123.<ACCOUNT_THUMBPRINT>Шаг 10. Первичный выпуск Let’s Encrypt сертификата
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:
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/Там должны быть как минимум:
- mail.domain.ru.cer
- mail.domain.ru.key
- ca.cer
- fullchain.cerШаг 11. Подготовка hot-update через socket
Добавляем acme в группу haproxy:
sudo usermod -aG haproxy acmeПроверяем socket после старта HAProxy:
ls -l /run/haproxy/admin.sockДля hot-update через deploy hook у HAProxy и acme.sh нужен доступ к stats socket.
Проверяем доступ socket от имени acme:
sudo -u acme socat - UNIX-CONNECT:/run/haproxy/admin.sock <<< "show info"Шаг 12. Deploy в HAProxy
Теперь, когда cert уже выпущен, кладём production PEM:
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Выставляем права:
sudo chown root:haproxy /etc/haproxy/certs/mail.domain.ru.pem
sudo chmod 660 /etc/haproxy/certs/mail.domain.ru.pemПроверяем, что deploy hook закрепил параметры:
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
# ==================== 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Проверка и запуск:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxyШаг 14. Автообновление
Добавляем cron от имени acme:
sudo crontab -u acme -eДобавляем:
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
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Потом проверяем внешний сертификат:
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
$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
$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 `
$cerPathCA-SERVER\CA-NAME — доменное имя центра сертификации
Шаг 3. Импортируем в Exchange
Import-ExchangeCertificate -FileData ([System.IO.File]::ReadAllBytes('C:\Cert\mail.domain.ru.cer'))Шаг 4. Проверяем сертификат и ищем thumbprint
Get-ExchangeCertificate | Format-List FriendlyName,Thumbprint,Subject,CertificateDomains,Services,NotAfter,StatusПроверяем содержимое:
SubjectсодержитCN=mail.domain.ruCertificateDomainsсодержит:mail.doamin.ruautodiscover.domain.ru
StatusнормальныйNotAfterсоответствует сроку нового cert
Шаг 5. Назначаем сертификат сервисам Exchange
Enable-ExchangeCertificate -Thumbprint <THUMBPRINT> -Services IIS,SMTP,IMAP,POPШаг 6. Экспорт сертификата Exchange в PFX
$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.mscTrusted Root Certification Authorities- выбрать root CA
All Tasks→ExportBase-64 encoded X.509 (.CER)
Передаем файл на сервер Haproxy:
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, обычно сработает так:
openssl x509 -in /home/user/domain-root-ca.cer -out /home/user/domain-root-ca.pemЕсли это DER, то так:
openssl x509 -in /home/user/domain-root-ca.cer -inform der -out /home/user/domain-root-ca.pemКладём root CA в отдельный каталог HAProxy:
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), который нужен только для локальной проверки доверия.
openssl pkcs12 -in /home/manager/mail.domain.ru.pfx -clcerts -nokeys -out /home/user/server-cert.pemПроверка доверия к внутреннему сертификату
Проверяем, что root CA действительно доверяет серверному cert Exchange:
openssl verify -CAfile /etc/haproxy/ca/domain-root-ca.pem /home/user/server-cert.pemОжидаемый ответ:
/home/user/server-cert.pem: OKBackend в HAProxy
Для чистой схемы лучше использовать явный CA-файл, а не системный bundle:
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
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
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
echo "show servers state" | socat stdio /run/haproxy/admin.sockТест 4. ACME state
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
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. Тест полной автоматики
openssl s_client -connect mail.domain.ru:443 -servername mail.domain.ru </dev/null | \
openssl x509 -noout -issuer -dates -fingerprintЕсли внешний сертификат меняется — автоматизация закрыта
