Сразу после установки mailcow я упёрся в первую настоящую загадку: сертификат Let's Encrypt не выпускался. HTTP-валидация падала.
Выглядело это как проблема с DNS или с портами — то есть ровно как то, что проверяешь первым делом. Записи на месте. Порты открыты. Домен снаружи открывается. Валидация падает.
Причина оказалась в слое, о котором я не думал вообще.
Коротко
- Контейнер
acme-mailcowрезолвит домен во внешний IP и идёт проверять себя.- Сервер за NAT не может достучаться до собственного внешнего адреса, если роутер не делает hairpin NAT (он же NAT loopback).
- Снаружи открыто, изнутри — нет. Ошибка выглядит как «что-то с DNS».
- Лечится добавлением
extra_hostsвdocker-compose.override.yml.
Что происходит на самом деле
Чтобы получить сертификат по HTTP-валидации, надо доказать, что вы управляете доменом. Контейнер acme-mailcow для этого обращается к собственному домену.
Домен резолвится во внешний IP — тот, что у роутера. Дальше запрос уходит наружу и должен вернуться обратно внутрь, на тот же сервер. Это и называется hairpin NAT, или NAT loopback: пакет разворачивается на роутере, как шпилька.
Многие роутеры так не умеют. И тогда получается положение, которое ставит в тупик: снаружи сайт открывается у кого угодно, а изнутри собственной сети — нет. Проверка доступности проваливается не потому, что домен недоступен, а потому, что он недоступен именно оттуда, откуда его проверяют.
Коварство в том, что симптом не указывает на причину никак. Вы видите неудачную валидацию и идёте проверять DNS-записи, открытые порты, фаервол, права на файлы — всё, что находится в порядке. Слово «NAT» в сообщении об ошибке не появляется.
Как проверить, ваш ли это случай
Проверка занимает минуту, и делать её надо изнутри того самого контейнера, который выпускает сертификат:
docker compose exec acme-mailcow getent hosts mail.example.com
# 203.0.113.25 — внешний IP, до которого изнутри не достучаться
Если в ответе внешний адрес — это ваш случай.
Разница между «проверил с ноутбука» и «проверил изнутри контейнера» здесь решающая. С ноутбука в той же сети результат может быть другим, с мобильного интернета — третьим. Проверять надо оттуда, откуда ходит тот, кто жалуется — а жалуется здесь контейнер.
Решение
Раз сервер не может дойти до себя длинным путём через роутер, пусть ходит коротким — по внутреннему адресу.
В docker-compose.override.yml для сервиса acme-mailcow добавляются extra_hosts: имена mail, autodiscover, autoconfig и mta-sts основного домена указываются на внутренний адрес самой виртуалки.
services:
acme-mailcow:
extra_hosts:
- "mail.example.com:10.10.10.10"
- "autodiscover.example.com:10.10.10.10"
- "autoconfig.example.com:10.10.10.10"
- "mta-sts.example.com:10.10.10.10"
Применяем и проверяем тем же запросом, что и раньше — теперь он должен отвечать внутренним адресом:
docker compose up -d acme-mailcow
docker compose exec acme-mailcow getent hosts mail.example.com # теперь 10.10.10.10
docker compose logs --tail=100 -f acme-mailcow
После этого контейнер при проверке идёт на себя напрямую, минуя роутер, и валидация проходит.
Порядок проверки тут не случайный: сначала убеждаемся, что имя резолвится по-новому, и только потом смотрим в лог. Иначе, увидев в логе ту же ошибку, непонятно, что именно не сработало — правка или выпуск.
Почему именно override, а не правка основного файла. Потому что docker-compose.override.yml не перетирается обновлениями mailcow. Правка, которую смоет ближайшим апдейтом, — это не решение, а отложенный инцидент: через полгода сертификат перестанет продлеваться, и разбираться придётся заново, уже забыв про этот случай.
Чего эта правка не лечит
Стоит отделить один случай от другого, потому что симптом у них общий — HTTP validation failed, — а причины разные.
HTTP-проверка ACME работает по 80-му порту. Не по 443, не по тому, на котором у вас сайт, а именно по 80: удостоверяющий центр обращается туда снаружи и ожидает получить файл, который положил ваш сервер.
Отсюда две разные поломки:
- порт 80 не доступен снаружи — центр вообще не может дойти до сервера.
extra_hostsтут ни при чём, чинить надо проброс и фаервол; - порт доступен снаружи, но сервер не видит себя изнутри — это случай из этой статьи.
Различаются они одной проверкой, и делать её надо снаружи вашей сети — с мобильного интернета, с чужого хоста, чем угодно, лишь бы не из той же сети. Проверка из офиса даст ответ про офис, а не про интернет.
И общий порядок, который экономит время: сначала убедитесь, что снаружи доходит, потом — что изнутри резолвится правильно. Если перепутать порядок, можно долго править extra_hosts там, где просто закрыт порт.
Результат
Сертификат выпустился на все четыре SAN: mail, autodiscover, autoconfig, mta-sts. Проверено, работает.
Я держу этот случай в памяти отдельным пунктом, потому что он относится к классу задач, где догадка почти гарантированно уводит не туда: в self-hosted почте за NAT ACME падает молча, если хост не видит свой внешний адрес изнутри.
И более общее, что я из этого забрал. Когда проверка не проходит, а всё проверяемое в порядке, стоит задать вопрос не «что сломано», а «откуда именно идёт проверка». Довольно часто ответ именно там: инструмент смотрит из точки, про которую вы не подумали.