mailcow restore: no private key available — причина
АйТи Фреш
Linux, Docker и DevOps

После восстановления mailcow старые письма не открываются: No private key available, хотя ключи скопированы

Автор: , директор ООО «АйТи-Фреш» · · ~16 мин чтения
Похожие, но разные пары ключей шифрования — восстановленный ключ не подходит к архивным письмам mailcow
Ключ на месте и рабочий, но это ключ от другой пары — поэтому старые письма не открываются.

Если вы восстановили mailcow из полного бэкапа, файлы ключей в crypt-vol-1 на месте, а старые письма всё равно выдают «No private key available», — ключи не потерялись: на сервере просто другая пара, не та, которой зашифрована старая переписка. Разбираю, откуда берётся рассинхронизация после восстановления, как её отличить от банальной потери ключей и что проверить, прежде чем объявлять архив нечитаемым.

Кейс: бэкап полный, восстановление прошло чисто, письма всё равно не читаются

«Проигрыватель и Ко» — магазин виниловых пластинок и техники, 37 рабочих мест, три точки продаж плюс интернет-магазин. mailcow у них стоял на арендованном сервере, и в сентябре хостер сообщил о плановой замене оборудования с полной пересборкой виртуалки. Мы ведём эту компанию по контракту на сопровождение корпоративной почты, и миграцию на новый сервер администратор клиента делал по нашему регламенту: прогнал helper-scripts/backup_and_restore.sh backup all перед переездом — то есть архивировались все компоненты разом, включая и vmail, и crypt, а не только почтовые файлы. На новом сервере восстановление тем же скриптом отработало без единой ошибки, все ящики появились, структура папок цела, счётчики писем сошлись день в день.

Проблема вскрылась не сразу, а когда бухгалтерия попыталась поднять переписку по возврату оборудования за прошлый квартал: письма в списке были видны — тема, отправитель, дата, — но при открытии Outlook и веб-почта SOGo одинаково выдавали ошибку чтения содержимого. В логе Dovecot при попытке открыть конкретное сообщение находилась строка вида Mailbox INBOX: UID=…: read() failed: … Decryption error: no private key available — ровно та же, что в Issue #5111 на GitHub mailcow (версия 2023-03, Debian 11), где восстановление тоже «прошло успешно». Свежие письма, пришедшие уже после восстановления, открывались нормально — проблема касалась именно архивной переписки, созданной до миграции.

На первый взгляд это выглядело как ровно та же беда, что уже разбиралась в статье про архив mailcow и ключи — там показан случай, когда скопировали только vmail, забыли crypt-vol-1, и расшифровать письма стало физически нечем. Но здесь ситуация другая и обиднее: архив backup_crypt в бэкапе есть, том crypt-vol-1 на новом сервере есть, файлы ecprivkey.pem и ecpubkey.pem в нём присутствуют. А расшифровка всё равно не идёт. Значит, дело не в отсутствии ключа, а в том, что имеющийся ключ не подходит к конкретным письмам — то есть в самой механике восстановления что-то развело пару ключей и зашифрованные ими данные.

Чем шифрование почты mailcow отличается от простого хранения файлов

В mailcow сообщения на диске не лежат обычным текстом Maildir, как многие привыкли по классическим связкам Postfix+Dovecot. По умолчанию письма хранятся сжатыми (lz4) и зашифрованными парой асимметричных ключей — это описано в разделе документации про Dovecot mail_crypt. Пара ключей физически находится в отдельном Docker-томе crypt-vol-1, отдельно от самого содержимого писем в vmail-vol-1. Такое разделение сделано осознанно: даже если у злоумышленника окажется копия файлов Maildir, без пары ключей из другого тома содержимое писем прочитать нельзя.

Ключевой нюанс для восстановления — это именно пара ключей, привязанная к конкретной инсталляции в момент её создания, а не к учётной записи или домену. Публичным ключом (ecpubkey.pem) шифруются новые письма по мере их поступления, приватным (ecprivkey.pem) — расшифровываются при чтении. Если в какой-то момент жизни сервера пара ключей меняется — например, том crypt-vol-1 был удалён и создан заново, или это результат смешивания файлов из разных резервных копий, — то новым ключом старые письма, зашифрованные прежней парой, прочитать физически невозможно, даже если новый ключ выглядит абсолютно корректным и рабочим для новых писем.

Документация отдельно предупреждает об этом жирным текстом в разделе про очистку постоянных данных: удаление тома crypt-vol-1 делает зашифрованные письма нечитаемыми навсегда, даже если vmail-vol-1 цел и невредим. То есть с точки зрения mailcow это не баг и не сбой при восстановлении, а прямое следствие того, как работает шифрование: ключ и данные должны происходить из одной и той же исходной пары, а не просто оба присутствовать на сервере.

Если при восстановлении хоть раз запускался mailcow с новым, ещё не заполненным crypt-vol-1 — хотя бы на секунду, для теста, — новая пара ключей уже могла быть сгенерирована автоматически. Именно это чаще всего и рвёт связь между старым архивом и текущими ключами.
Схема: письма в vmail-vol-1 зашифрованы парой ключей из crypt-vol-1, оба тома работают только вместе
vmail без своей пары ключей из crypt-vol-1 — просто набор нечитаемых зашифрованных файлов.

Где реально рвётся связь ключ-данные при восстановлении

По моему опыту разбора подобных инцидентов, есть три типичных сценария, из-за которых восстановленные ключи технически присутствуют, но не подходят к архиву. Первый — порядок операций при восстановлении. Если на новом сервере сначала подняли mailcow «вчистую», дав docker compose up -d без предварительного восстановления crypt-vol-1 из бэкапа, Dovecot при первом старте молча генерирует новую пару ключей: в docker-entrypoint.sh контейнера стоит проверка — если /mail_crypt/ecprivkey.pem или ecpubkey.pem отсутствует или пуст, создаётся новая пара на кривой prime256v1. Если потом восстановить из бэкапа и «Crypt data», скрипт остановит dovecot-mailcow и перезапишет оба файла архивными — это безопасно. Беда начинается, когда crypt при восстановлении пропускают, потому что «ключи и так есть»: интерактивный restore восстанавливает за один проход либо один выбранный компонент, либо все сразу (пункт 0 - all), и пропустить один пункт очень легко.

Второй сценарий — смешивание бэкапов от разных дат или разных серверов. Если vmail восстановлен из одной точки бэкапа, а crypt — из другой (например, при ручном восстановлении по частям, а не единым прогоном backup_and_restore.sh restore), высок риск взять письма от одной генерации ключей, а ключи — от другой, более поздней или более ранней. Внешне обе копии выглядят абсолютно валидными бэкапами mailcow, и только при попытке расшифровки конкретных писем обнаруживается несовпадение.

Третий, более редкий сценарий — использование doveadm force-resync в процессе восстановления для «починки» индекса ящиков. Force-resync полезен, когда Dovecot не видит физически присутствующие письма (это отдельная и в целом безопасная операция, разобрана в статье про force-resync), но он работает с индексами, а не с содержимым, и сам по себе ключи не портит. Индексы в mailcow лежат в отдельном томе vmail-index-vol-1, и их чистка ключи не трогает. Тем не менее в моей практике встречался случай, когда администратор перед force-resync удалял тома по маске в самописном скрипте и заодно снёс crypt-vol-1, после чего Dovecot при старте создал новую пару — итог был тем же самым no private key available, хотя причина формально не в шифровании, а в человеческой ошибке при ручной чистке.

Отличить эти сценарии друг от друга по одному только сообщению об ошибке нельзя — оно одинаковое для всех трёх причин. Дальше нужна конкретная диагностика: сравнить, что физически лежит в текущем crypt-vol-1, с тем, что было заархивировано, и одинаковы ли отпечатки ключей.

Дерево решений: три причины расхождения ключей шифрования после восстановления mailcow и как их отличить
Три разные причины дают один и тот же текст ошибки — различать их приходится по отпечатку ключа.

Как проверить, какая именно пара ключей сейчас активна

Первый шаг — просто убедиться, что файлы ключей физически на месте и не пустые. Документация mailcow (раздел Mail crypt) показывает, как расшифровать конкретный файл письма через doveadm fs get с путями к обоим ключам — я использую ту же команду как тест «читается ли письмо текущей парой», направляя вывод в head, а не в файл:

docker compose exec dovecot-mailcow bash
# внутри контейнера: путь к конкретному старому письму в Maildir
doveadm fs get compress lz4:1:crypt:private_key_path=/mail_crypt/ecprivkey.pem:public_key_path=/mail_crypt/ecpubkey.pem:posix:prefix=/ \
  "/var/vmail/example.com/user/Maildir/cur/<имя_файла>" | head -n 20

Если этот же ключ читает свежие письма, но не читает старые из того же ящика — уже прямое подтверждение, что дело не в отсутствии ключа как файла, а в несовпадении конкретно этой пары с конкретными старыми сообщениями. Дальше сравниваем отпечаток текущего ключа с тем, что лежит в архиве backup_crypt (в свежих версиях скрипта это .tar.zst, в старых — .tar.gz), если у вас сохранились несколько версий бэкапа за разные даты:

# отпечаток публичного ключа, который сейчас работает в dovecot-mailcow
docker compose exec -T dovecot-mailcow cat /mail_crypt/ecpubkey.pem | openssl pkey -pubin -outform DER | sha256sum

# то же для ключа из архива (распакуйте backup_crypt.tar.zst во временную папку)
mkdir -p /tmp/restore_check && tar --use-compress-program=zstd -xf /opt/backup/mailcow-<дата-время>/backup_crypt.tar.zst -C /tmp/restore_check
openssl pkey -pubin -in /tmp/restore_check/crypt/ecpubkey.pem -outform DER | sha256sum

Ключ -pubin обязателен: без него openssl попытается прочитать файл как приватный ключ, упадёт, и если ошибку спрятать в 2>/dev/null, вы получите одинаковый хеш пустой строки для обоих файлов и ложное «совпало». Совпадение отпечатков означает, что ключ действительно тот же самый и проблему нужно искать не в шифровании, а, например, в правах доступа контейнера dovecot-mailcow к тому crypt-vol-1 или в повреждении конкретных файлов писем. Расхождение отпечатков подтверждает основную версию: на сервере сейчас работает другая пара ключей, чем та, которой зашифрован архив, и восстанавливать переписку нужно именно последней корректной резервной копией crypt-vol-1, синхронной по времени с нужными письмами — через backup_and_restore.sh restore с пунктом «Crypt data» или all из той же точки бэкапа, а не ручным копированием файлов.

В «Проигрывателе и Ко» отпечатки разошлись: администратор действительно поднимал mailcow на новом сервере на пробу за два дня до основной миграции, чтобы проверить сеть и DNS, и Dovecot успел сгенерировать собственную пару ключей при первом старте. В день миграции он запускал restore несколько раз, по компоненту за проход — «Mail directory», «SQL DB», «Redis DB», — а «Crypt data» пропустил: ключи в томе уже были, и казалось, что восстанавливать их незачем. Письма за сутки после миграции (около 400) оказались зашифрованы тестовой парой. Поэтому сначала мы выгрузили их через IMAP во временный архив (при чтении по IMAP Dovecot отдаёт письма расшифрованными), сохранили тестовый crypt-vol-1 отдельно, затем восстановили «Crypt data» из бэкапа и вернули свежие письма обратно. Архивная переписка открылась, потерь не было; на всё ушло около трёх часов.

Никогда не поднимайте mailcow «вчистую» на целевом сервере для тестов сети/DNS перед восстановлением из бэкапа — или хотя бы восстанавливайте потом с пунктом `all`. Даже кратковременный старт без подложенного crypt-vol-1 создаёт новую пару ключей, и если Crypt data при restore пропустить, архивная переписка не откроется.
Сравнение двух исходов проверки отпечатка ключа шифрования mailcow: совпадение и расхождение, что делать в каждом случае
Одна команда с openssl sha256 сразу показывает, в какую сторону вести дальнейшую диагностику.

Как я строю восстановление, чтобы не словить эту рассинхронизацию

Правило номер один — восстановление всегда единым прогоном официального скрипта, а не ручным копированием отдельных томов через docker cp или rsync по частям. helper-scripts/backup_and_restore.sh restore в интерактивном режиме сначала предлагает выбрать точку восстановления, затем набор данных; пункт 0 - all разворачивает все компоненты из одного среза за один проход. Выбор по одному пункту допустим, но именно на нём и теряют «Crypt data».

Правило номер два — если новому серверу нужно предварительно проверить сеть, DNS, сертификаты или что-то ещё до финального восстановления, делайте это на отдельном тестовом инстансе mailcow с собственным доменом и данными, никогда не разворачивая продуктивные ящики на «пустой» инсталляции, которая потом будет донакатываться бэкапом. Если тестовый запуск на целевом сервере неизбежен — перед основным восстановлением явно удалите том crypt-vol-1 (и, на всякий случай, vmail-vol-1), созданный в ходе теста, командой docker volume rm при остановленном стеке (docker compose down); имена томов имеют префикс проекта, например mailcowdockerized_crypt-vol-1, чтобы восстановление точно разворачивалось на чистом месте, а не поверх уже существующих данных.

Правило номер три — храните резервные копии vmail и crypt синхронно по времени и никогда не смешивайте компоненты от разных дат вручную, даже если кажется, что «почта не менялась, а ключи точно старые актуальнее». Ключи и данные — это одна логическая единица с точки зрения шифрования, и относиться к ним нужно как к неделимой паре, а не как к двум независимым бэкапам, которые можно комбинировать по своему усмотрению.

И четвёртое, для более широкой картины: если в компании кроме почты есть и другие зашифрованные системы резервного копирования — например Veeam с включённым шифрованием заданий, — там действует похожая логика «ключ и данные должны происходить из одной генерации», просто механика другая. У меня есть отдельный разбор именно такого случая — что происходит со старыми копиями при включении шифрования Veeam, полезно держать в голове как общий принцип при построении любой схемы бэкапов с шифрованием, не только почтовой.

Частые вопросы

Ключи на месте, а письма всё равно дают no private key available — как такое возможно?

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

Чем это отличается от ситуации, когда просто забыли скопировать ключи при бэкапе?

Это разные проблемы с одинаковым симптомом. Если ключей нет вообще — расшифровать нечем в принципе, это разобрано в статье про архив mailcow и ключи. Здесь ключи технически есть, но это другая пара, не та, которой зашифрован конкретный архив, — из-за рассинхронизации в процессе восстановления.

Как проверить, что на сервере сейчас именно та пара ключей, которой зашифрован архив?

Сравните отпечаток публичного ключа в контейнере и в архиве backup_crypt: openssl pkey -pubin -in ecpubkey.pem -outform DER | sha256sum для обоих файлов. Совпадение означает, что ключ тот же, и причину нужно искать не в шифровании; ключ -pubin обязателен, иначе openssl выдаст ошибку.

Можно ли сгенерировать новую пару ключей вручную и как-то перешифровать старые письма?

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

Безопасно ли поднимать mailcow на новом сервере для теста DNS и сети перед основной миграцией?

Только если это отдельный тестовый инстанс с собственными данными. Если тест идёт на целевом сервере, который затем будет принимать восстановление из бэкапа, перед основным restore нужно явно удалить тома crypt-vol-1 и vmail-vol-1, созданные тестовым запуском, командой docker volume rm при остановленном стеке — иначе восстановление может лечь на уже существующие данные и ключи.

Влияет ли doveadm force-resync на ключи шифрования?

Сама по себе force-resync работает с индексами ящиков (в mailcow они в отдельном томе vmail-index-vol-1), а не с ключами шифрования, и напрямую их не портит. Проблемы возникают, только если при ручных манипуляциях заодно удаляется том crypt-vol-1 — тогда Dovecot при старте создаёт новую пару.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи