Выберите продукт

HAProxy + Let's Encrypt: Certbot и acme.sh, TLS, SNI и map без боли

Разбираем рабочий прод-стек: как настроить HAProxy как единственную точку терминации TLS, подружить его с Let's Encrypt через Certbot или acme.sh, отдать HTTP-01-челленджи, развести десятки доменов по SNI и map-файлам и автоматизировать выпуск сертификатов без даунтайма.
HAProxy + Let's Encrypt: Certbot и acme.sh, TLS, SNI и map без боли

HAProxy давно перестал быть просто балансировщиком: на многих инсталляциях он одновременно и L7 reverse-proxy, и точка терминации TLS, и место, где сходятся десятки доменов с разными сертификатами. В такой схеме вопрос «как именно прикрутить Let's Encrypt» уже не праздный — от него зависят надёжность, удобство автоматизации и даже архитектура.

В этой статье разберём практичный стек:

  • HAProxy как точка терминации TLS для множества доменов.
  • Let's Encrypt с двумя клиентами: Certbot и acme.sh.
  • HTTP-01 и TLS-ALPN-01, встраивание /.well-known/acme-challenge в конфиг.
  • SNI и директивы crt/crt-list/crt-base.
  • Использование map для динамического выбора backend и логики.
  • Ротация сертификатов без даунтайма и перезапусков.

Фокус будет на живых примерах конфигурации, а не теории TLS.

Где заканчивается TLS: HAProxy как единственная точка терминации

Первый архитектурный вопрос: где именно терминировать TLS? На уровне приложений (Nginx/Apache/Node.js) или прямо в HAProxy? В большинстве сценариев удобнее вынести TLS в HAProxy:

  • Единая точка управления шифрами, версиями TLS и политикой.
  • Простая интеграция с Let's Encrypt: весь трафик проходит через одну точку.
  • Проще SNI-мультиплексинг десятков доменов.
  • Быстрая ротация сертификатов: перезагрузили только HAProxy — и готово.

Дальше будем исходить из того, что TLS-терминация происходит в frontend https-in, а к backend-сервисам ходим по HTTP (внутренний трафик). Если вам нужно TLS и до backend'ов — это отдельная тема, но базовые принципы работы с сертификатами те же.

Если раздаёте несколько проектов с одного сервера (или кластера), существенно упростить жизнь поможет заранее продуманная структура доменов и сертификатов. Для части проектов подойдёт общий wildcard, для части — отдельные DV или SAN. Отдельно можно посмотреть подводные камни мультидоменных и SAN-сертификатов в связке с автоматизацией в материале про автоматизацию цепочки DV/SAN-сертификатов.

Let's Encrypt: Certbot или acme.sh для HAProxy

У Let's Encrypt десятки клиентов, но в реальности админы чаще всего выбирают между Certbot и acme.sh.

Certbot: плюсы и нюансы с HAProxy

Certbot — «официальный» клиент, доступен в репозиториях большинства дистрибутивов и хорошо задокументирован. Однако он заточен под веб-серверы вроде Nginx/Apache, а для HAProxy приходится либо использовать --standalone, либо писать обвязку.

Типичные варианты интеграции:

  • standalone: Certbot поднимает временный HTTP-сервер на 80-м порту, HAProxy на время освобождает порт.
  • webroot: Certbot кладёт challenge-файлы в webroot backend'а, а HAProxy просто проксирует /.well-known/acme-challenge до него.
  • dns-01: через DNS-плагины, если нужен wildcard и нет нормального HTTP-доступа.

Для HAProxy чаще всего удобен режим webroot, потому что он не требует остановки HAProxy и не лезет в его конфигурацию.

acme.sh: простота, один файл сертификата и cron

acme.sh — лёгкий shell-клиент для ACME, популярный именно в связке с прокси и балансировщиками. Его плюсы в контексте HAProxy:

  • Не тянет тяжёлые зависимости (Python и т.п.).
  • Легко заставить класть сертификаты сразу в удобном для HAProxy виде fullchain.pem + privkey.pem или объединённый .pem.
  • Простой cron-режим без демонов.
  • Много готовых хуков для DNS-API.

Поскольку HAProxy умеет работать с «склеенными» файлами, где в одном .pem лежит и сертификат, и цепочка, и ключ, с acme.sh получается особенно аккуратная интеграция: выпускаем сертификат, склеиваем (или настраиваем прямо так), кладём в директорию, на которую смотрит crt/crt-list, делаем reload HAProxy.

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

Диаграмма проксирования HTTP-01-челленджа Let's Encrypt через HAProxy в backend

Базовый TLS-frontend c SNI

Перед тем как трогать Let's Encrypt, стоит привести в порядок базовую TLS-конфигурацию HAProxy. Простой пример для двух доменов:

frontend https-in
  bind *:443 ssl crt /etc/haproxy/certs/example.com.pem crt /etc/haproxy/certs/api.example.com.pem alpn h2,http/1.1
  mode http

  http-request redirect scheme https code 301 if !{ ssl_fc }

  acl host_web hdr(host) -i example.com
  acl host_api hdr(host) -i api.example.com

  use_backend web if host_web
  use_backend api if host_api

backend web
  server web1 127.0.0.1:8080 check

backend api
  server api1 127.0.0.1:8081 check

Когда доменов становится больше, хранить каждый crt в bind-строке неудобно — лучше использовать crt-list или crt-base:

frontend https-in
  bind *:443 ssl crt-list /etc/haproxy/crt-list.txt alpn h2,http/1.1
  mode http

Содержимое /etc/haproxy/crt-list.txt:

/etc/haproxy/certs/example.com.pem
/etc/haproxy/certs/api.example.com.pem
/etc/haproxy/certs/other-site.com.pem

Либо так:

frontend https-in
  bind *:443 ssl crt-base /etc/haproxy/certs alpn h2,http/1.1

В этом случае HAProxy просто берёт все .pem-файлы из каталога.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

HTTP-01: как отдать /.well-known/acme-challenge через HAProxy

Для HTTP-01-челленджа Let's Encrypt нужно, чтобы по адресу

http://example.com/.well-known/acme-challenge/<token>

отдавался определённый контент. Если у вас уже есть HAProxy, который слушает 80-й порт, логично просто проксировать этот путь до backend'а с статикой или до встроенного в Certbot/acme.sh webroot.

Пример схемы:

  • HAProxy слушает 80/443.
  • Есть отдельный backend acme, который ведёт на маленький HTTP-сервер (например, на Nginx или встроенный в Certbot webroot).
  • Все запросы /.well-known/acme-challenge на любом домене уводим в этот backend.
frontend http-in
  bind *:80
  mode http

  acl acme_challenge path_beg /.well-known/acme-challenge/
  use_backend acme if acme_challenge

  # дальше обычные редиректы на https или проксирование
  http-request redirect scheme https code 301 if !acme_challenge

backend acme
  server acme1 127.0.0.1:8085 check

На 8085 порту крутится простейший HTTP-сервер, откуда Certbot или acme.sh забирают/кладут challenge-файлы. Например, Nginx с корнем /var/www/acme, а клиент запускаем с --webroot /var/www/acme (Certbot) или с аналогичным параметром webroot у acme.sh.

Если вам важно поддерживать HSTS и жёсткий редирект на HTTPS, аккуратнее отлаживать такую схему заранее и не забывать про особые случаи (например, health-check-и внешних мониторингов на 80-м порту). Про HSTS и связанные с ним подводные камни в Let's Encrypt-инфраструктуре мы отдельно разбирали в статье о Certbot и строгом HTTPS.

Терминал администратора с командами Certbot и acme.sh для автоматизации SSL в HAProxy

Использование map для маршрутизации по доменам

Когда доменов десятки, писать кучу acl host_xxx hdr(host) и use_backend быстро превращается в ад. Для этого в HAProxy есть map-файлы: внешние таблицы соответствий, которые можно менять без переписывания основного конфига.

Простейший пример: есть файл /etc/haproxy/maps/hosts.map с таким содержимым:

example.com web
api.example.com api
static.example.com static

В frontend пишем:

frontend https-in
  bind *:443 ssl crt-base /etc/haproxy/certs alpn h2,http/1.1
  mode http

  http-request set-var(txn.backend_name) map(/etc/haproxy/maps/hosts.map,lower,hdr(host))
  use_backend %[var(txn.backend_name)] if { var(txn.backend_name) -m found }

  default_backend web

Теперь, чтобы привязать новый домен к backend, достаточно дописать одну строку в hosts.map и перезагрузить HAProxy. Точно так же можно использовать map для разных флагов: включение HSTS, разных timeouts по доменам, логгирования и т.п.

Если вы используете HAProxy как фронт к нескольким приложениям с разными доменами на одном IP, не забывайте и про грамотное управление самими доменами: удобнее вести их в одном аккаунте и держать под рукой автообновление NS и WHOIS. В этом хорошо помогает централизованная регистрация доменов для всех проектов.

Как хранить сертификаты для множества доменов

Стандартная схема хранения сертификатов под HAProxy:

  • Один каталог, например /etc/haproxy/certs.
  • Для каждого домена один .pem-файл: example.com.pem, api.example.com.pem и т.д.
  • Внутри файла: приватный ключ, затем сертификат домена, затем цепочка.

Порядок важен: HAProxy ожидает сначала -----BEGIN PRIVATE KEY----- или аналогичный блок, затем -----BEGIN CERTIFICATE----- (сертификат домена), затем промежуточные.

Если клиент (Certbot/acme.sh) по умолчанию кладёт файлы отдельно, имеет смысл сделать пост-обработку: собирать единый .pem и класть в /etc/haproxy/certs. Это удобно делать в deploy-hook или reload-hook.

Интеграция HAProxy + Certbot на практике

Возьмём пример с Certbot в режиме webroot и одним доменом example.com.

Шаг 1: готовим webroot и backend

Создаём директорию для челленджей и backend в HAProxy, как описано выше:

mkdir -p /var/www/acme/.well-known/acme-challenge

Поднимаем простой HTTP-сервер (любой на ваш вкус) на 8085 с корнем в /var/www/acme, либо используем уже существующий.

Шаг 2: первый выпуск сертификата Certbot

Запускаем Certbot, указав webroot:

certbot certonly --webroot -w /var/www/acme -d example.com -d www.example.com

После успешного выпуска сертификаты появятся в стандартном каталоге /etc/letsencrypt/live/example.com/. Нам нужно склеить их в один .pem для HAProxy:

cat /etc/letsencrypt/live/example.com/privkey.pem /etc/letsencrypt/live/example.com/fullchain.pem > /etc/haproxy/certs/example.com.pem

Не забываем выставить права так, чтобы HAProxy мог читать, но лишние пользователи — нет. Часто достаточно группы haproxy и chmod 640.

Шаг 3: конфиг HAProxy с новым сертификатом

В frontend https-in добавляем путь к файлу (если используем crt-base, он уже подхватится автоматически):

frontend https-in
  bind *:443 ssl crt-base /etc/haproxy/certs alpn h2,http/1.1
  mode http
  # остальная логика

Перезагружаем HAProxy с проверкой конфига:

haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl reload haproxy

Шаг 4: автоматизация продления и релоада HAProxy

Certbot обычно ставит cron/systemd-таймер, который вызывает certbot renew. Нам надо ещё и обновлять .pem-файл HAProxy и делать reload после успешного продления.

Создадим простой скрипт, который будет вызываться как --deploy-hook:

#!/bin/sh
DOMAIN="$CERTBOT_DOMAIN"
LIVE_DIR="/etc/letsencrypt/live/$DOMAIN"
HAPROXY_PEM="/etc/haproxy/certs/$DOMAIN.pem"

if [ -f "$LIVE_DIR/privkey.pem" ] && [ -f "$LIVE_DIR/fullchain.pem" ]; then
  cat "$LIVE_DIR/privkey.pem" "$LIVE_DIR/fullchain.pem" > "$HAPROXY_PEM"
  chmod 640 "$HAPROXY_PEM"
  chown root:haproxy "$HAPROXY_PEM"
  haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl reload haproxy
fi

Ставим права на исполнение и прописываем его при выпуске сертификата:

chmod +x /usr/local/bin/certbot-haproxy-hook.sh
certbot certonly --webroot -w /var/www/acme -d example.com -d www.example.com --deploy-hook /usr/local/bin/certbot-haproxy-hook.sh

В дальнейшем certbot renew сам будет вызывать хук при успешном продлении.

Интеграция HAProxy + acme.sh

С acme.sh концепция похожа, но интеграция с HAProxy зачастую получается чище, так как клиент легко настраивается на вывод уже «правильного» формата.

Установка и базовая настройка acme.sh

Устанавливать можно разными способами, но принцип один: скрипт кладётся в домашнюю директорию и регистрируется cron для обновлений.

Далее настраиваем домен с HTTP-01 через webroot, как в примере с Certbot, либо сразу используем DNS-API, если нужен wildcard и есть подходящая интеграция с вашим DNS-провайдером.

Выпуск сертификата и direct-вывод в каталог HAProxy

acme.sh позволяет указать путь и формат установки сертификатов для конкретного домена. Например:

acme.sh --issue -d example.com -d www.example.com --webroot /var/www/acme

После успешного выпуска делаем установку в нужный формат:

acme.sh --install-cert -d example.com --key-file /etc/haproxy/certs/example.com.key --fullchain-file /etc/haproxy/certs/example.com.fullchain.pem --reloadcmd "cat /etc/haproxy/certs/example.com.key /etc/haproxy/certs/example.com.fullchain.pem > /etc/haproxy/certs/example.com.pem && chmod 640 /etc/haproxy/certs/example.com.pem && chown root:haproxy /etc/haproxy/certs/example.com.pem && haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl reload haproxy"

Таким образом, при каждом продлении acme.sh автоматически перегенерирует ключ и цепочку, соберёт .pem и перезагрузит HAProxy.

FastFox VDS
Регистрация доменов от 99 руб.
Каждый проект заслуживает идеального доменного имени, выберите один из сотни, чтобы начать работу!

TLS-ALPN-01 и HAProxy

Помимо HTTP-01, есть челлендж TLS-ALPN-01: Let's Encrypt подключается по 443 порту и ожидает, что сервер ответит специальным сертификатом с расширением ACME. С HAProxy это уже ощутимо сложнее, потому что нужно уметь подменять сертификат на время проверки.

Поддержка TLS-ALPN-01 для HAProxy возможна через использование отдельных bind'ов или динамической подстановки сертификатов, но такой сценарий гораздо более специфичен и сложен в отладке. На практике, если есть возможность использовать HTTP-01 или DNS-01, лучше сделать так и не усложнять конфигурацию.

Для проектов, где 80-й порт принципиально недоступен, а DNS-API по каким-то причинам использовать нельзя, TLS-ALPN-01 действительно может быть единственным вариантом, но это уже тема отдельной, узкоспециализированной статьи.

Wildcard-сертификаты: когда без DNS-01 не обойтись

Если у вас SaaS с десятками субдоменов вроде client1.example.com, client2.example.com и т.п., wildcard-сертификат *.example.com сильно упрощает жизнь. Let's Encrypt выдаёт wildcard только через DNS-01-челлендж, т.е. клиент должен уметь создавать TXT-записи в зоне.

И Certbot, и acme.sh имеют плагины и скрипты для взаимодействия с популярными DNS-провайдерами (Cloudflare, PowerDNS API и т.д.). С HAProxy сценарий выглядит так:

  1. Клиент выпускает wildcard через DNS-01 (никакой HTTP-трафик не нужен).
  2. Собирает сертификат и ключ в единый .pem и кладёт в /etc/haproxy/certs/wildcard-example.com.pem.
  3. HAProxy в bind или crt-base подхватывает этот файл и обслуживает все субдомены.

Если при этом у вас есть ещё и отдельные сертификаты для конкретных доменов (например, example.com без звёздочки), HAProxy по SNI выберет наиболее подходящий сертификат, поэтому можно спокойно комбинировать wildcard и обычные.

Тонкости reload и zero-downtime

Перезапуск HAProxy ради обновления сертификатов — не лучшая идея в продакшене. К счастью, HAProxy умеет «мягкий» reload без обрыва активных соединений, если правильно настроить systemd-юнит.

Основные моменты:

  • Используйте systemctl reload haproxy, а не restart.
  • Перед reload делайте проверку конфигурации haproxy -c -f /etc/haproxy/haproxy.cfg.
  • Не меняйте пути к сертификатам динамически во время продления — только содержимое файлов.

При такой схеме Certbot или acme.sh могут безопасно дёргать reload после успешного продления, а HAProxy подхватит новые сертификаты без даунтайма.

Использование map для гибких TLS-настроек

map-файлы полезны не только для маршрутизации по backend, но и для гибкого управления TLS-поведением по доменам. Например, вы можете включать HSTS только для доменов, где уже точно везде работает HTTPS, или задавать разные security-профили.

Пример: карта, где указан режим HSTS по доменам:

/etc/haproxy/maps/hsts.map:
example.com on
secure.example.com on
legacy.example.com off

В frontend:

http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" if { hdr(Host),lower -m map(/etc/haproxy/maps/hsts.map) -m str on }

Та же идея применима и к другим заголовкам, redirect-политикам и даже выбору backend'а в зависимости от того, TLS это или нет.

Диагностика проблем TLS и SNI

Когда что-то идёт не так с TLS в HAProxy, чаще всего виноваты:

  • Неправильный формат .pem-файла (не тот порядок блоков, лишние строки).
  • Отсутствие цепочки (клиент видит self-signed или unknown CA).
  • Неправильный SNI (клиент шлёт один hostname, а вы ожидаете другой).
  • HAProxy стартует раньше, чем сертификат создан или имеет нужные права.

Для диагностики удобно использовать openssl s_client и указывать нужный SNI:

openssl s_client -connect example.com:443 -servername example.com -showcerts

Смотрите, какой именно сертификат отдаётся, совпадают ли CN/SAN, полна ли цепочка и т.д. Если у вас несколько доменов на одном IP, обязательно проверяйте каждый с правильным -servername, иначе получите «первый попавшийся» сертификат по умолчанию.

Итоги

HAProxy отлично дружит с Let's Encrypt, если заранее продумать схему:

  • Определяем, что TLS-терминация живёт в HAProxy, backend'ы получают HTTP.
  • Используем HTTP-01 через webroot или DNS-01 для wildcard — TLS-ALPN-01 оставляем для особых случаев.
  • Выбираем клиент: Certbot (стабильный и привычный) или acme.sh (лёгкий и очень гибкий).
  • Храним сертификаты в виде отдельных .pem на домен, подключаем через crt-base или crt-list.
  • Автоматизируем склейку и ротацию через deploy-hooks, делая только безопасный reload HAProxy.
  • Используем map-файлы для масштабируемой маршрутизации и пер-доменных политик.

Грамотная интеграция даёт вам десятки (или сотни) доменов на одном HAProxy с полностью автоматическим выпуском и обновлением сертификатов, без ручных правок конфигов при каждом новом домене и без неожиданных даунтаймов из-за истёкшего SSL.

Поделиться статьей

Вам будет интересно

Ansible inventory и group_vars для staging и production: структура проекта без хаоса OpenAI Статья написана AI (GPT 5)

Ansible inventory и group_vars для staging и production: структура проекта без хаоса

Когда Ansible-проект растёт, хаос обычно начинается с inventory и переменных: staging и production смешиваются, а итоговое значени ...
Linux: /etc/fstab и emergency mode (rescue) — как быстро поднять систему после ошибки монтирования OpenAI Статья написана AI (GPT 5)

Linux: /etc/fstab и emergency mode (rescue) — как быстро поднять систему после ошибки монтирования

Если после перезагрузки Linux падает в emergency mode или rescue из‑за /etc/fstab, чаще всего виноваты неверный UUID, опции монтир ...
Linux: когда зависает PID 1 (systemd) — D-state, stop-jobs и hung_task OpenAI Статья написана AI (GPT 5)

Linux: когда зависает PID 1 (systemd) — D-state, stop-jobs и hung_task

Если растёт load average, сервисы не останавливаются, а перезагрузка висит на stop jobs — часто виноваты D-state и блокировки I/O. ...