После восстановления mailcow старые письма не открываются: No private key available, хотя ключи скопированы
Если вы восстановили 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 в нём присутствуют. А расшифровка всё равно не идёт. Значит, дело не в отсутствии ключа, а в том, что имеющийся ключ не подходит к конкретным письмам — то есть в самой механике восстановления что-то развело пару ключей и зашифрованные ими данные.
- backup_and_restore.sh backup all — заархивированы все компоненты, включая vmail и crypt
- восстановление прошло без ошибок скрипта, структура папок и счётчики писем корректны; backup_crypt в архиве есть
- свежие письма открываются, старые (до миграции) — нет, с ошибкой Decryption error: no private key available
- это НЕ случай «ключи не скопировали» — файлы 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 это не баг и не сбой при восстановлении, а прямое следствие того, как работает шифрование: ключ и данные должны происходить из одной и той же исходной пары, а не просто оба присутствовать на сервере.
- письма хранятся сжатыми lz4 и зашифрованными, а не обычным текстом Maildir
- пара ключей (ecprivkey.pem/ecpubkey.pem) лежит в отдельном томе crypt-vol-1
- ключи привязаны к конкретной паре, созданной один раз, а не переиздаются автоматически под каждое восстановление
- удаление или подмена 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, с тем, что было заархивировано, и одинаковы ли отпечатки ключей.
- сценарий 1: mailcow поднят «вчистую», а Crypt data при restore пропущен → работает пара, созданная Dovecot при первом старте
- сценарий 2: vmail и crypt восстановлены из разных по времени/источнику бэкапов вручную, а не единым backup_and_restore.sh restore
- сценарий 3: самописная чистка томов задела crypt-vol-1 — Dovecot создал новую пару
- сообщение об ошибке одинаковое для всех причин — дальше нужна диагностика, а не гадание
Как проверить, какая именно пара ключей сейчас активна
Первый шаг — просто убедиться, что файлы ключей физически на месте и не пустые. Документация 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» из бэкапа и вернули свежие письма обратно. Архивная переписка открылась, потерь не было; на всё ушло около трёх часов.
- doveadm fs get с указанием private_key_path — прямая проверка, читается ли конкретное письмо текущим ключом
- сравнение отпечатков публичного ключа (openssl pkey -pubin + sha256sum) между сервером и архивом бэкапа — быстрый способ подтвердить рассинхронизацию
- совпадение отпечатков → ищите проблему в правах доступа или повреждении файлов писем, а не в шифровании
- расхождение отпечатков → нужен чистый повторный restore именно crypt-vol-1 из синхронной резервной копии
- перед заменой ключей выгрузите по IMAP письма, пришедшие уже на новой паре, — иначе они станут нечитаемыми
Как я строю восстановление, чтобы не словить эту рассинхронизацию
Правило номер один — восстановление всегда единым прогоном официального скрипта, а не ручным копированием отдельных томов через 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, полезно держать в голове как общий принцип при построении любой схемы бэкапов с шифрованием, не только почтовой.
- restore — через helper-scripts/backup_and_restore.sh с пунктом all, не по частям вручную
- тестовый прогон mailcow на целевом сервере до восстановления — только с явным docker volume rm перед основным restore
- vmail и crypt всегда восстанавливаются из одной и той же по времени точки бэкапа, не смешиваются вручную
- принцип «ключ и данные — неделимая пара» актуален для любой системы с шифрованием бэкапов, не только для mailcow
Частые вопросы
Ключи на месте, а письма всё равно дают 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 при старте создаёт новую пару.
Источники
- docs.mailcow.email — Mail crypt (Dovecot) — Проверено: письма хранятся сжатыми lz4 и зашифрованными, пара ключей в томе crypt-vol-1, пути /mail_crypt/ecprivkey.pem и /mail_crypt/ecpubkey.pem, команда doveadm fs get compress lz4:1:crypt:private_key_path=…:public_key_path=…:posix:prefix=/. https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-mail-crypt/
- docs.mailcow.email — Remove Persistent Data — Проверено прямое предупреждение: удаление тома crypt-vol-1 делает зашифрованные письма нечитаемыми навсегда, даже если vmail-vol-1 сохранён. https://docs.mailcow.email/troubleshooting/debug-rm_volumes/
- GitHub mailcow-dockerized — helper-scripts/backup_and_restore.sh — Проверено содержимое скрипта: компоненты (vmail, crypt, redis, rspamd, postfix, mysql, all), тома ${CMPS_PRJ}_crypt-vol-1, restore останавливает dovecot-mailcow и распаковывает архив поверх тома, интерактивный выбор точки и одного набора данных или «0 - all». Также docker-entrypoint.sh dovecot: генерация пары prime256v1 при отсутствии ключей. https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
- mailcow community — Mail cant be read/decrypt after restore — Проверен разбор практического случая расхождения ключей бэкапа и восстановленной установки с ошибкой no private key available. https://community.mailcow.email/d/2236-mail-cant-be-readdecrypt-after-restore
- GitHub mailcow-dockerized — Issue #5111 — Проверено: mailcow 2023-03, Debian 11, после восстановления backup_vmail (отдельно и вместе с backup_crypt) ошибка «read() failed … Decryption error: no private key available», issue закрыт как stale без решения. https://github.com/mailcow/mailcow-dockerized/issues/5111



