Как выпустить сертификат Let's Encrypt в mailcow при закрытом порте 80
Если входящий TCP/80 закрыт, больше не нужно прикручивать к mailcow отдельный certbot или acme.sh. Начиная с версии 2026-03, выпущенной 10 марта 2026 года, mailcow умеет проходить DNS-01 штатно. Я, Семёнов Евгений Сергеевич, покажу вариант, который применяю сам: обновляем mailcow, выдаём узкий API-токен для DNS, включаем встроенный ACME-контейнер и обязательно проверяем сертификат на HTTPS, SMTP и IMAP.
Короткий ответ: штатный DNS-01 уже есть
Старые инструкции с отдельным ACME-клиентом не были ошибочными. Они просто описывали mailcow до марта 2026 года. Тогда встроенный acme-mailcow рассчитывал прежде всего на HTTP-01: центр сертификации обращался к специальному файлу по TCP/80. Если оператор связи, межсетевой экран или архитектура с внешним балансировщиком не пропускали этот запрос, администратор ставил certbot, lego либо отдельный acme.sh, копировал сертификаты в data/assets/ssl и добавлял post-hook для перезагрузки сервисов. Работало. Но получалась вторая система автоматизации, о которой через полгода неизбежно забывали.
В mailcow 2026-03 появился встроенный DNS-01. Контейнер создаёт TXT-запись _acme-challenge через API DNS-провайдера, Let's Encrypt проверяет её в публичной DNS и выдаёт сертификат. Входящее соединение на порт 80 при этом не используется. На 8 сентября 2026 года актуальная стабильная редакция — 2026-07b, и я выбираю именно обновление на поддерживаемую stable-ветку, а не остановку на первом релизе 2026-03. После мартовского релиза уже выходили исправления ACME и обновления безопасности.
Позиция у меня простая: если сертификаты выпускает сам mailcow, пусть он же управляет их продлением и перезагрузкой Postfix, Dovecot и Nginx. Отдельный клиент оставляю только там, где сертификатами централизованно управляет внешний reverse proxy или корпоративная PKI. Для обычного почтового узла самостоятельный certbot — теперь лишняя движущаяся часть. Он добавляет секреты, таймер, сценарий копирования и ещё одну точку отказа, но не даёт заметного выигрыша.
- Минимальная версия для встроенного DNS-01 — mailcow 2026-03.
- Для продуктивной системы следует ставить актуальную стабильную ревизию, а не первоначальный 2026-03.
- Входящий TCP/80 для DNS-01 не нужен.
- Исходящий доступ к Let's Encrypt и API DNS-провайдера всё равно необходим.
- Настройка DNS-01 распространяется на все домены экземпляра mailcow.
Что проверяет DNS-01 и где находятся ограничения
При HTTP-01 Let's Encrypt должен получить файл именно через порт 80. Переназначить проверку на произвольный внешний порт нельзя. При DNS-01 доказательством владения становится TXT-запись под именем _acme-challenge соответствующего домена. Поэтому сервер может находиться за NAT, порт 80 можно фильтровать на периметре, а веб-интерфейс — публиковать только через VPN или внешний прокси. Наличие публичного A- или AAAA-адреса у проверяемого имени само по себе для DNS-01 не является способом проверки.
Есть важное ограничение mailcow: режим выбирается для экземпляра целиком. Если задано ACME_DNS_CHALLENGE=y, все имена сертификата проходят DNS-01; совместить HTTP-01 для одного домена и DNS-01 для другого в штатной конфигурации нельзя. Переменная SKIP_HTTP_VERIFICATION в таком режиме игнорируется, потому что HTTP-проверка уже не выполняется. Это регулярно упускают: администратор добавляет три строки в mailcow.conf, оставляет часть доменов у другого DNS-провайдера и удивляется, почему единый заказ сертификата не проходит.
До изменения я выписываю все имена, которые mailcow собирается включить в сертификат: MAILCOW_HOSTNAME, явно заданные ADDITIONAL_SAN, а также подходящие autodiscover, autoconfig и при соответствующей настройке mta-sts для почтовых доменов. Затем проверяю, что выбранный API-токен способен создать TXT-запись во всех нужных DNS-зонах. Если зоны обслуживаются разными провайдерами, один глобально указанный плагин ACME_DNS_PROVIDER задачу обычно не закроет. Я переношу зоны к одному управляемому провайдеру или делегирую _acme-challenge в отдельную зону. Это уже архитектурное решение, а не ещё один флаг mailcow.
- Проверьте NS-записи каждого почтового домена: важен авторитетный DNS, а не регистратор и не локальный DNS-сервер.
- Уберите из `ADDITIONAL_SAN` имена, которые фактически не используются.
- Проверьте CAA, если организация ограничивает допустимые центры сертификации.
- Убедитесь, что часы узла синхронизированы и исходящие HTTPS-запросы не блокируются.
- Заранее решите, один ли DNS-провайдер обслуживает все зоны сертификата.
- Для wildcard помните: `*.example.com` не покрывает сам `example.com`, в `ADDITIONAL_SAN` нужно указать оба имени.
Подготовка версии и API-токена
Перед настройкой я обновляю mailcow штатным update.sh. Сначала делаю резервную копию и читаю изменения между установленной и текущей stable-версией. Обновление почтового комплекса — не место для импровизации: версии до 2026-03 вообще не знают новых переменных, а первоначальные 2026-03 и 2026-03a получили последующие исправления. Для предварительной проверки и обновления из каталога установки использую:
cd /opt/mailcow-dockerized
./update.sh --check
./update.shПосле запуска проверяю версию в интерфейсе и состояние контейнеров командой docker compose ps. Если сервер давно не обновлялся, сначала отрабатываю переход на копии или тестовом узле.
По ресурсам DNS-01 почти ничего не меняет, но сам mailcow остаётся тяжёлым групповым решением. Официальный минимум — 1 ГГц CPU, 6 ГиБ RAM плюс 1 ГиБ swap и 20 ГиБ диска без учёта почты. Для компании с 15 телефонами на ActiveSync и примерно 50 одновременными IMAP-подключениями документация рекомендует планировать 16 ГиБ RAM. Я не пытаюсь экономить здесь пару гигабайт: нехватка памяти ударит по SOGo, ClamAV и индексации, а администратор по ошибке начнёт связывать эти симптомы с переходом на DNS-01.
Для примера беру Cloudflare, потому что синтаксис хорошо документирован. Создаю не Global API Key, а отдельный API Token по шаблону редактирования DNS. Разрешение — Zone > DNS > Edit, область — только конкретная зона kardan-opt.example. Идентификатор аккаунта сохраняю отдельно. Токен не отправляю в мессенджер, не кладу в систему резервного копирования без шифрования и не вставляю в заявку технической поддержки. Компрометация DNS-токена опаснее, чем многие думают: злоумышленник сможет влиять не только на выпуск сертификата, если права выданы на весь аккаунт.
- Сделать проверяемую резервную копию mailcow перед обновлением.
- Перейти на актуальную stable-версию не ниже 2026-03.
- Создать отдельный токен, а не использовать глобальный ключ Cloudflare.
- Ограничить токен конкретной DNS-зоной и правом изменения DNS.
- Зафиксировать владельца токена и процедуру его ротации.
Рабочая конфигурация mailcow
В mailcow.conf включаю DNS-проверку, выбираю плагин Cloudflare и задаю адрес ACME-аккаунта. Адрес должен быть действующим: на него могут приходить уведомления, связанные с учётной записью центра сертификации. В нашем условном примере базовая часть файла выглядит так:
MAILCOW_HOSTNAME=mail.kardan-opt.example
ADDITIONAL_SAN=imap.kardan-opt.example,smtp.kardan-opt.example
ACME_DNS_CHALLENGE=y
ACME_DNS_PROVIDER=dns_cf
ACME_ACCOUNT_EMAIL=postmaster@kardan-opt.exampleНе добавляйте wildcard просто потому, что DNS-01 его поддерживает. Если пользователи обращаются только к перечисленным именам, обычный SAN-сертификат понятнее и ограничивает область возможной ошибки.
Реквизиты провайдера размещаю не в mailcow.conf, а в data/conf/acme/dns-01.conf. Для Cloudflare официальный пример использует именно CF_Token и CF_Account_ID:
CF_Token="значение_выданного_API_токена"
CF_Account_ID="идентификатор_аккаунта_Cloudflare"Файл не должен быть доступен обычным пользователям хоста. После создания проверяю владельца и ставлю режим 600 командой chmod 600 data/conf/acme/dns-01.conf. Это моя эксплуатационная гигиена: mailcow должен прочитать секрет, но локальным пользователям он не нужен.
Применяю конфигурацию штатным Compose, затем смотрю журнал ACME-контейнера:
cd /opt/mailcow-dockerized
docker compose up -d
docker compose logs --tail=200 -f acme-mailcowЕсли действующий сертификат ещё не требует обновления, одного up -d может быть недостаточно для наглядного теста. Тогда инициирую принудительный выпуск поддерживаемым способом:
cd /opt/mailcow-dockerized
touch data/assets/ssl/force_renew
docker compose restart acme-mailcow
docker compose logs --tail=200 -f acme-mailcowФайл force_renew контейнер удалит автоматически. Многократно повторять эту операцию нельзя: лишние попытки приближают ограничения Let's Encrypt и засоряют диагностику.
- `ACME_DNS_CHALLENGE=y` включает DNS-01.
- `ACME_DNS_PROVIDER=dns_cf` выбирает Cloudflare-плагин acme.sh.
- `ACME_ACCOUNT_EMAIL` задаёт адрес ACME-аккаунта.
- `data/conf/acme/dns-01.conf` содержит секреты DNS-провайдера.
- `docker compose logs -f acme-mailcow` показывает создание TXT, проверку и установку сертификата.
Если mailcow старше 2026-03 или сертификат выпускает внешний клиент
Штатный DNS-01 доступен не всегда: узел может сидеть на старой версии, которую пока нельзя обновить, или сертификат уже выпускает корпоративный acme.sh на отдельной машине. Тогда встроенный ACME-клиент нужно выключить, иначе он будет пытаться пройти HTTP-01 через закрытый порт 80 и засорять журнал. В mailcow.conf задаю SKIP_LETS_ENCRYPT=y и применяю изменение через docker compose up -d. После этого mailcow перестаёт заказывать сертификаты и использует те файлы, которые лежат в data/assets/ssl.
По документации Advanced SSL сервисы mailcow читают сертификат из data/assets/ssl/cert.pem, а закрытый ключ — из data/assets/ssl/key.pem. Файлы нужно именно копировать, а не делать символические ссылки: внутри контейнеров путь за пределами смонтированного каталога не существует. В cert.pem я кладу полную цепочку (fullchain), иначе часть почтовых клиентов и сторонних MTA будет ругаться на неполную цепочку. Пример для acme.sh с тем же плагином Cloudflare:
export CF_Token="значение_API_токена"
export CF_Account_ID="идентификатор_аккаунта"
acme.sh --issue --dns dns_cf -d mail.kardan-opt.example -d autodiscover.kardan-opt.example -d autoconfig.kardan-opt.example
acme.sh --install-cert -d mail.kardan-opt.example \
--fullchain-file /opt/mailcow-dockerized/data/assets/ssl/cert.pem \
--key-file /opt/mailcow-dockerized/data/assets/ssl/key.pem \
--reloadcmd "/usr/local/sbin/mailcow-cert-reload.sh"Для certbot аналог — пакет certbot-dns-cloudflare и ключ --dns-cloudflare-credentials с файлом прав 600; копирование в data/assets/ssl тогда делает deploy-hook.
Самое важное в этой схеме — перезапуск сервисов после замены файлов. Именно на нём ломалась автоматизация в разобранном выше кейсе. Документация mailcow предписывает перезапустить Postfix, Nginx и Dovecot; сценарий mailcow-cert-reload.sh у меня состоит ровно из этих команд:
#!/bin/sh
docker restart $(docker ps -qaf name=postfix-mailcow)
docker restart $(docker ps -qaf name=nginx-mailcow)
docker restart $(docker ps -qaf name=dovecot-mailcow)Перезапуск Dovecot рвёт активные IMAP-сессии, клиенты переподключатся сами, но запускать хук лучше ночью, а не в разгар приёма заявок. После первого прогона обязательно проверяю сертификат на 443, 587 и 993 так же, как описано ниже. Когда узел всё-таки обновят до 2026-03 или новее, возвращаю SKIP_LETS_ENCRYPT=n, включаю DNS-01 и только после успешного выпуска удаляю внешний клиент.
- `SKIP_LETS_ENCRYPT=y` полностью отключает встроенный ACME-клиент mailcow.
- Сертификат — `data/assets/ssl/cert.pem` (полная цепочка), ключ — `data/assets/ssl/key.pem`.
- Файлы копировать, а не связывать символическими ссылками.
- После замены перезапустить `postfix-mailcow`, `nginx-mailcow` и `dovecot-mailcow`.
- Секреты DNS-плагина хранить с правами `600` и только на той машине, где работает клиент.
Практика: «Кардан-Опт», оптовый склад и 37 рабочих мест
Разберу обезличенный проект; название «Кардан-Опт» условное. Оптовый склад автозапчастей: центральный склад, офис продаж и пункт выдачи, 37 рабочих мест, 44 почтовых ящика (включая общие zakaz@ и sklad@), 19 смартфонов с ActiveSync у менеджеров и экспедиторов и примерно 1900 входящих и исходящих писем в рабочий день — заявки клиентов, прайсы поставщиков, счета. mailcow работал на виртуальной машине Ubuntu 24.04 LTS: 6 vCPU, 16 ГиБ RAM, 200 ГиБ SSD. Узел имел один публичный IPv4. Порты 25, 465, 587, 993 и 443 были опубликованы, а TCP/80 блокировался правилами площадки. Открывать его заказчик не хотел, и для этой схемы это было вполне разумно.
До переделки там оставался mailcow 2025-12 и внешний ACME-клиент. Раз в два месяца администратор вручную проверял сертификат, потому что post-hook после копирования иногда не перезагружал Dovecot. Веб-интерфейс уже показывал новый сертификат, а IMAP продолжал отдавать старый. Outlook предупреждал пользователей, телефоны просили подтвердить недоверенный сервер, и служба поддержки получала шесть-восемь одинаковых обращений за утро — в основном от менеджеров, которые с утра открывают заявки с телефона. Это типичный результат половинчатой автоматизации: файл обновили, жизненный цикл сертификата целиком — нет.
В апреле, в согласованное окно в субботу, когда склад не отгружает, мы сделали резервную копию, обновили систему до mailcow 2026-03b и удалили старый systemd timer только после фиксации его команд и путей. Позже узел штатно обновили до 2026-07b; настройки DNS-01 при этом переносить не пришлось. В сертификат вошли mail.kardan-opt.example, smtp.kardan-opt.example, imap.kardan-opt.example, autodiscover.kardan-opt.example и autoconfig.kardan-opt.example. Все имена относились к одной Cloudflare-зоне. Первый запуск завершился ошибкой API: токен создали с правом просмотра DNS вместо редактирования. Это повторяется из раза в раз — человек видит слово DNS и не проверяет действие. После выпуска токена с Zone > DNS > Edit контейнер создал TXT-записи, дождался их доступности и получил сертификат.
От начала успешной попытки до установки сертификата прошло около трёх минут; конкретное время зависит от DNS-провайдера и распространения записей. Мы проверили HTTPS, SMTP Submission с STARTTLS и IMAPS, затем оставили систему под наблюдением. Через цикл автоматического продления сертификат обновился без открытия порта 80 и без ручного копирования файлов. Старый ACME-клиент, его таймер и скрипт перезагрузки удалили после контрольного периода. С апреля сертификат продлевался уже несколько раз, и обращений из-за него не было ни одного. Для меня это и есть критерий результата: не красивый лог в момент внедрения, а отсутствие ручной процедуры при очередном продлении.
- Склад, офис продаж и пункт выдачи, 37 рабочих мест.
- 44 почтовых ящика и 19 мобильных подключений.
- ВМ: 6 vCPU, 16 ГиБ RAM, SSD 200 ГиБ.
- mailcow обновлён с 2025-12 до 2026-03b, затем до стабильной 2026-07b.
- Пять DNS-имён в одном сертификате.
- Входящий TCP/80 оставлен закрытым.
Как проверить результат и не пропустить следующее продление
Зелёной строки в логе недостаточно. Сначала смотрю, какой сертификат реально отдаёт HTTPS:
openssl s_client -connect mail.kardan-opt.example:443 \
-servername mail.kardan-opt.example </dev/null 2>/dev/null |
openssl x509 -noout -subject -issuer -dates -ext subjectAltNameЗатем тем же принципом проверяю SMTP Submission и IMAPS:
openssl s_client -starttls smtp -connect mail.kardan-opt.example:587 \
-servername mail.kardan-opt.example </dev/null 2>/dev/null |
openssl x509 -noout -issuer -dates -ext subjectAltName
openssl s_client -connect mail.kardan-opt.example:993 \
-servername mail.kardan-opt.example </dev/null 2>/dev/null |
openssl x509 -noout -issuer -dates -ext subjectAltNameСравниваю notAfter и SAN во всех трёх ответах. Разные даты обычно означают, что один из сервисов не перечитал сертификат или трафик пришёл на другой узел.
Если выпуск не проходит, начинаю не с переустановки контейнера, а с журнала acme-mailcow. Ошибка авторизации API указывает на токен, аккаунт или область зоны. Сообщение о невозможности найти TXT чаще означает неверный авторитетный DNS, медленное распространение либо split DNS. Снаружи проверяю запись командами dig TXT _acme-challenge.mail.kardan-opt.example и dig +trace _acme-challenge.mail.kardan-opt.example. Локальный резолвер может уже видеть запись, а авторитетные серверы — отвечать не все. У anycast-провайдеров наблюдение из одной точки также не гарантирует, что тот же ответ увидит Let's Encrypt.
После внедрения ставлю мониторинг срока действия минимум на 443, 587 и 993 с предупреждениями заранее. Отдельно контролирую завершение acme-mailcow без ошибок и документирую ротацию API-токена. Проверку после ротации выполняю принудительным выпуском один раз в согласованное окно. На временные TXT-записи обычно можно не тратить отдельный проект мониторинга: клиент создаёт и удаляет их сам. А вот забытый истекающий сертификат игнорировать нельзя — он одновременно затронет веб-клиент, почтовые программы и мобильные устройства.
Отдельно учитываю сроки жизни сертификатов. Сейчас стандартный профиль Let's Encrypt classic выдаёт сертификаты на 90 дней, но сокращение уже официально объявлено: с 13 мая 2026 года на 45 дней перешёл профиль tlsserver, который включается только по выбору клиента; 10 февраля 2027 года classic перейдёт на 64 дня с 10-дневным повторным использованием авторизаций, а 16 февраля 2028 года — на 45 дней с повторным использованием всего 7 часов. Для DNS-01 последнее означает, что практически каждое продление будет заново создавать TXT-записи через API. Поэтому токен с истёкшим сроком или отозванными правами всплывёт быстро, а порог мониторинга я ставлю не «за 7 дней», а хотя бы за 14–20 дней до окончания действия.
Итоговый выбор такой: для mailcow 2026-03 и новее при закрытом порте 80 я использую штатный DNS-01. Это меньше кода, меньше ручных перезагрузок и один понятный владелец жизненного цикла сертификата. Спорное место одно — DNS-токен хранится на почтовом сервере. Я принимаю этот риск при узких правах токена и нормальной защите узла. Открывать порт 80 только ради ACME или сохранять отдельный клиент теперь считаю менее аккуратным решением.
- Проверить SAN, издателя и срок сертификата на HTTPS.
- Отдельно проверить SMTP/STARTTLS и IMAPS.
- Настроить предупреждение до истечения сертификата.
- Следить за ошибками `acme-mailcow`.
- После ротации API-токена выполнить один контролируемый тест.
- Записать процедуру восстановления DNS-доступа в эксплуатационную документацию.
Частые вопросы
Нужно ли открывать входящий порт 80 для штатного DNS-01 в mailcow?
Нет. DNS-01 подтверждает владение доменом через TXT-запись в публичной DNS. Однако серверу нужен исходящий доступ к Let's Encrypt и API DNS-провайдера.
С какой версии mailcow доступен встроенный DNS-01?
С версии 2026-03, выпущенной 10 марта 2026 года. Для эксплуатации я рекомендую актуальную стабильную ревизию, потому что после первого релиза выходили исправления ACME и безопасности.
Можно ли одному домену назначить HTTP-01, а другому DNS-01?
Не в штатной конфигурации одного экземпляра mailcow. При `ACME_DNS_CHALLENGE=y` DNS-01 применяется ко всем доменам экземпляра.
Нужен ли теперь отдельный certbot или acme.sh?
Обычно нет: mailcow сам использует ACME-компоненты, получает сертификат и обслуживает его жизненный цикл. Внешний клиент оправдан, если сертификатами централизованно управляет reverse proxy или отдельная PKI.
Куда класть сертификат, если его выпускает acme.sh или certbot?
В `data/assets/ssl/cert.pem` (полная цепочка) и `data/assets/ssl/key.pem` каталога mailcow, копированием, а не символической ссылкой. Встроенный клиент отключается `SKIP_LETS_ENCRYPT=y`, а после замены файлов нужно перезапустить контейнеры postfix-mailcow, nginx-mailcow и dovecot-mailcow.
Какие параметры нужны для Cloudflare?
В `mailcow.conf` задаются `ACME_DNS_CHALLENGE=y`, `ACME_DNS_PROVIDER=dns_cf` и `ACME_ACCOUNT_EMAIL`. В `data/conf/acme/dns-01.conf` размещаются `CF_Token` и `CF_Account_ID`.
Почему TXT виден через dig, но Let's Encrypt не принимает проверку?
Проверьте ответы всех авторитетных NS, а не только локального кеширующего резолвера. Причиной бывают задержка распространения, split DNS, неверная делегация или разные ответы anycast-узлов.
Сертификаты Let's Encrypt станут короче — нужно ли что-то менять?
По официальному объявлению Let's Encrypt профиль classic перейдёт на 64 дня 10 февраля 2027 года и на 45 дней 16 февраля 2028 года. При работающем автоматическом продлении менять конфигурацию не требуется, но стоит проверить мониторинг срока действия и срок жизни DNS API-токена.
Источники
- mailcow Documentation — SSL with DNS Challenge, Configuration, Supported DNS Providers и Configuration Examples; актуальная документация для mailcow 2026-03 и новее — https://docs.mailcow.email/post_installation/firststeps-ssl-dns/
- mailcow Release Notes — Moorch 2026, релизы 2026-03 от 10 марта, 2026-03a и 2026-03b; появление и исправления DNS-01 — https://mailcow.email/posts/2026/release-2026-03/
- mailcow Release Notes — Mooly 2026, редакция 2026-07b от 18 августа 2026 года — https://mailcow.email/posts/2026/release-2026-07/
- mailcow Documentation — Advanced SSL: SKIP_LETS_ENCRYPT, ADDITIONAL_SAN, Force renewal, использование собственных сертификатов (data/assets/ssl/cert.pem, key.pem) и перезапуск postfix/nginx/dovecot — https://docs.mailcow.email/post_installation/firststeps-ssl/
- Let's Encrypt Blog — Decreasing Certificate Lifetimes to 45 Days (2 декабря 2025): даты для профилей tlsserver и classic, сроки повторного использования авторизаций — https://letsencrypt.org/2025/12/02/from-90-to-45
- mailcow Documentation — Prepare your system, Minimum System Resources и RAM usage examples — https://docs.mailcow.email/getstarted/prerequisite-system/
- mailcow Documentation — Update, штатный сценарий update.sh и варианты stable, legacy, nightly — https://docs.mailcow.email/maintenance/update/
- Let's Encrypt Documentation — Challenge Types, разделы HTTP-01 и DNS-01; обновлено 12 февраля 2026 года — https://letsencrypt.org/docs/challenge-types/
- acme.sh Source Code — Cloudflare DNS API plugin dns_cf.sh, переменные CF_Token, CF_Account_ID и CF_Zone_ID — https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_cf.sh
- Certbot Documentation — certbot-dns-cloudflare, параметр --dns-cloudflare-credentials и права на файл учётных данных — https://certbot-dns-cloudflare.readthedocs.io/en/stable/
