Почему копии vmail недостаточно для чтения архива mailcow
Вы скопировали `vmail-vol-1`, нашли привычные каталоги `cur`, `new` и `tmp`, а вместо письма почтовый клиент показывает ошибку или бессмысленный бинарный поток. Это не повреждённый Maildir. В mailcow содержимое писем сжимается LZ4 и шифруется Dovecot, а ключи лежат в другом Docker-томе. Я, Семёнов Евгений Сергеевич, при проверке резервных копий начинаю именно с этой связки: данные плюс ключи. Ниже покажу, что сохранить, как быстро проверить архив и почему успешный `rsync` ещё ничего не доказывает.
Maildir есть, но письма в нём не обычные
Главная ловушка — каталог действительно имеет структуру Maildir. Видны домены, почтовые ящики, Maildir/cur, Maildir/new, служебные файлы Dovecot и имена сообщений с флагами после :2,. Администратор переносит один такой файл на рабочую станцию, переименовывает в .eml и пробует открыть Thunderbird. Не получается. Расширение тут ничего не меняет: заголовки RFC 5322 и MIME находятся внутри зашифрованной полезной нагрузки.
В штатном data/conf/dovecot/dovecot.conf mailcow задаёт глобальную пару EC-ключей (mail_crypt_global_private_key, mail_crypt_global_public_key) и mail_crypt_save_version = 2, а zlib_save = lz4 включает сжатие. Сжатие применяется до шифрования, поэтому на диске лежит уже зашифрованный поток. В актуальном docker-compose.yml mailcow том vmail-vol-1 подключён к контейнеру как /var/vmail, отдельный crypt-vol-1 — как /mail_crypt. Поэтому копия первого тома сохраняет байты писем и дерево каталогов, но не даёт средства преобразовать эти байты обратно в читаемый MIME.
На диске зашифрованное письмо обычно начинается сигнатурой CRYPTED. Затем Dovecot должен применить правильный закрытый ключ и снять слой сжатия. ecpubkey.pem позволяет шифровать новые данные, но сам по себе не расшифрует старые. Критически важен ecprivkey.pem; открытый ключ из него технически можно получить заново, однако я всегда сохраняю исходную пару целиком. Стоимость — несколько килобайт. Цена ошибки — весь архив.
- `vmail-vol-1` — каталоги почтовых ящиков и зашифрованные объекты писем.
- `crypt-vol-1` — глобальная пара `ecprivkey.pem` и `ecpubkey.pem`.
- `vmail-index-vol-1` — перестраиваемые индексы; для расшифровки писем не нужен.
Какие данные я считаю полноценным архивом
Для чтения отдельных писем минимальный комплект состоит из vmail и исходной пары ключей. База MariaDB для самой криптографической операции не нужна: имея файл, Dovecot и правильные ключи, можно получить .eml. Но это только технический архив сообщений. В нём нет полноценного описания действующей почтовой системы — учётных записей, алиасов, квот, доменных настроек и других объектов mailcow.
Для аварийного восстановления сервиса я сохраняю vmail, crypt и mysql как неразделимый набор, а в штатном задании выбираю all. Скрипт кладёт в точку восстановления также mailcow.conf, но отдельно я копирую каталог установки /opt/mailcow-dockerized: там могут находиться локальные конфиги, хуки, сертификаты и изменения, которых нет в архивах Docker-томов. Это уже не вопрос чтения письма, а вопрос воспроизводимости всей системы.
Остальные компоненты скрипта тоже стоит понимать поимённо. backup_and_restore.sh принимает vmail, crypt, redis, rspamd, postfix, mysql и all. mysql — дамп базы с доменами, ящиками, алиасами и квотами; redis хранит часть настроек и статистики (в том числе ключи DKIM, которые mailcow держит в Redis); rspamd — обученные байесовские данные и нечёткие хеши антиспама; postfix — очередь и данные Postfix. Без redis новый сервер поднимется, но DKIM-подписи придётся перевыпускать и заново публиковать в DNS — для небольшой фирмы это лишний день разбирательств с отказами доставки.
Закрытый ключ нельзя бездумно складывать рядом с незашифрованной внешней копией vmail: получив оба компонента, злоумышленник получает возможность читать почту. Я держу рабочий набор в зашифрованном хранилище резервных копий, а контрольную копию пары ключей — в отдельном защищённом хранилище с ограниченным доступом. Это спорный компромисс между доступностью и секретностью, но потерять единственный ключ из-за отказа одного массива гораздо реальнее, чем кажется.
- В первую очередь: `vmail` и `crypt` в одной согласованной точке восстановления.
- Для запуска mailcow: добавить `mysql`, остальные компоненты и каталог установки.
- Отдельно: зашифрованная контрольная копия двух PEM-файлов и записанные контрольные суммы.
- Можно не спасать любой ценой: почтовые индексы, кэш и временные файлы, если их можно перестроить.
Как за пять минут поставить диагноз
Сначала я ничего не расшифровываю и не изменяю. Проверяю, какой том подключён к Dovecot, существуют ли оба ключа и начинается ли выбранный файл с CRYPTED. Делать это надо внутри контейнера: пути /var/vmail и /mail_crypt принадлежат его файловой системе. На Docker-хосте /var/vmail вполне закономерно может отсутствовать.
Проверка для mailcow с Compose Plugin выглядит так:
cd /opt/mailcow-dockerized
docker inspect "$(docker compose ps -q dovecot-mailcow)" \
--format '{{range .Mounts}}{{println .Destination .Name}}{{end}}'
docker compose exec dovecot-mailcow sh -lc '
test -s /mail_crypt/ecprivkey.pem &&
test -s /mail_crypt/ecpubkey.pem &&
echo "key pair exists"
'
docker compose exec dovecot-mailcow \
head -c 7 /var/vmail/example.com/user/Maildir/cur/NAME-OF-MESSAGEПоследняя команда должна вывести CRYPTED для зашифрованного объекта. Имя реального файла лучше получить через find, а не набирать вручную.
Следом проверяю соответствие открытого ключа закрытому. Обе команды нормализуют публичную часть и считают её хеш; строки должны совпасть:
docker compose exec dovecot-mailcow sh -lc '
openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -outform PEM | sha256sum
openssl pkey -pubin -in /mail_crypt/ecpubkey.pem -pubout -outform PEM | sha256sum
'Совпавшие хеши подтверждают пару, но ещё не доказывают, что именно ею зашифрован архив. Окончательный тест — расшифровать одно старое и одно свежее письмо. Если новое читается, а старое нет, я ищу замену ключей или смешение поколений резервных копий.
- Том Dovecot: `docker inspect` должен показать `crypt-vol-1` в `/mail_crypt` и `vmail-vol-1` в `/var/vmail`.
- Ключи: оба PEM-файла не пустые, публичная часть закрытого ключа совпадает с `ecpubkey.pem`.
- Объект письма: первые семь байт — `CRYPTED`; если там сразу видны заголовки, письмо сохранено до включения шифрования.
- Финальный тест: одно старое и одно свежее письмо расшифровываются через `doveadm fs get`.
- Архив: `tar -tf` по `backup_crypt.tar.zst` показывает оба PEM-файла.
Как я строю резервное копирование mailcow
Мой штатный выбор — backup all. Он менее изящен, зато убирает человеческую ошибку из ежедневной процедуры. Восемь виртуальных CPU позволяют выделить шесть потоков: документация рекомендует задавать THREADS как число ядер минус два, чтобы mailcow не остался без процессора. Пример задания:
cd /opt/mailcow-dockerized
install -d -m 700 /mnt/backup/mailcow
MAILCOW_BACKUP_LOCATION=/mnt/backup/mailcow THREADS=6 \
./helper-scripts/backup_and_restore.sh backup all --delete-days 30Удаление старых точек я включаю только после появления второй, независимой политики хранения. Иначе одна ошибка расписания аккуратно удалит последнюю полезную копию.
Если объём или окно копирования не позволяют брать всё ежедневно, допустимый минимум для почтового восстановления выглядит так:
MAILCOW_BACKUP_LOCATION=/mnt/backup/mailcow THREADS=6 \
./helper-scripts/backup_and_restore.sh backup vmail crypt mysqlЗдесь принципиально, что vmail и crypt перечислены отдельно. В актуальном backup_and_restore.sh это разные ветви: первая создаёт backup_vmail.tar.zst, вторая — backup_crypt.tar.zst. Команда backup vmail не подразумевает crypt и не переносит ключи автоматически.
После задания я проверяю не только наличие файлов. zstd -t выявляет повреждение контейнера, а скрипт кладёт каждую точку в отдельный каталог mailcow-ДАТА внутри MAILCOW_BACKUP_LOCATION; список tar подтверждает присутствие обоих PEM-файлов, а SHA-256 фиксирует состав точки восстановления. Раз в квартал поднимаю чистый изолированный экземпляр, восстанавливаю архив и открываю несколько сообщений разных лет с вложениями. Это занимает часы, зато даёт измеримый RTO. Отчёт «backup completed» такой гарантии не даёт.
B=$(ls -d /mnt/backup/mailcow/mailcow-* | tail -n 1)
zstd -t "$B"/backup_vmail.tar.zst "$B"/backup_crypt.tar.zst
tar --zstd -tf "$B"/backup_crypt.tar.zst | grep -E 'ec(priv|pub)key\.pem'
sha256sum "$B"/* > "$B".sha256- Контролировать свободное место до запуска и размер результата после него.
- Передавать резервные копии по защищённому каналу и шифровать хранилище.
- Хранить минимум одну копию вне Docker-хоста и одну — вне основной площадки.
- После обновления mailcow повторять тест восстановления, а не только тест создания архива.
Стенд аудиторской фирмы «Проводка»: 96 ГиБ писем и забытый crypt
Приведу обезличенную реконструкцию типового проекта; «Проводка» — условное название. Аудиторская фирма на 19 рабочих мест: 24 ящика (сотрудники плюс общие audit@, docs@, buh@), около 610 тыс. файлов сообщений. Для аудиторов почта — рабочий архив: переписка с клиентами, запросы документов, акты сверки, заключения за прошлые годы, которые хранят не меньше пяти лет. Почтовый узел — виртуальная машина с 4 vCPU, 16 ГиБ RAM и 500 ГиБ SSD, mailcow в штатной сборке. Размер vmail-vol-1 достиг 96 ГиБ. Резервная копия уходила ночью на NAS через самодельный rsync.
Приходящий администратор проверял число каталогов и общий объём, поэтому восемь месяцев отчёты были зелёными. Когда понадобилось поднять переписку ушедшего партнёра по проверке 2023 года на отдельной машине, дерево Maildir оказалось на месте, но ни одно письмо не открылось. Первые семь байт показали CRYPTED. На NAS были vmail и каталог установки, а crypt-vol-1 не копировался: его размер в несколько килобайт приняли за служебную мелочь. Именно так эта ошибка обычно и возникает.
Исходный сервер ещё работал, поэтому мы успели забрать правильную пару из /mail_crypt. На изолированной виртуальной машине с 4 vCPU, 8 ГиБ RAM и 250 ГиБ SSD развернули ту же версию mailcow, восстановили vmail, crypt и mysql, не публикуя SMTP и веб-интерфейс наружу. Для ежедневного задания зафиксировали команду (четыре ядра минус два — два потока):
cd /opt/mailcow-dockerized
MAILCOW_BACKUP_LOCATION=/backup/mailcow THREADS=2 \
./helper-scripts/backup_and_restore.sh backup vmail crypt mysql redisРаз в неделю к этому добавили backup all, а копию каталога установки отправили отдельным заданием.
Полное тестовое восстановление заняло 2 часа 10 минут. Мы проверили 120 сообщений за разные годы, включая PDF-заключения и XLSX-выгрузки из учётных систем клиентов, затем выгрузили нужный ящик в переносимый незашифрованный архив. Потерь не обнаружили. После проекта ключевую пару положили во второе зашифрованное хранилище, доступ разделили между управляющим партнёром и администратором, а ежеквартальную проверку чтения внесли в регламент. Повезло только в одном: исходный Docker-том ещё существовал. Если бы сервер уже списали, 96 ГиБ копии за пять лет аудиторской переписки были бы практически бесполезны.
- Причина: в самодельный `rsync` попал только `vmail-vol-1`, том `crypt-vol-1` пропустили.
- Почему не заметили: мониторинг проверял объём и число каталогов, а не чтение письма.
- Исправление: штатный `backup_and_restore.sh` с явным `vmail crypt mysql redis` и еженедельный `all`.
- Контроль: контрольная копия PEM-файлов отдельно от NAS и квартальный тест восстановления.
Как восстановить письмо или сделать переносимый Maildir
Я восстанавливаю архив только в изолированный экземпляр mailcow. На новой системе сначала инициализирую и запускаю пустой стек, как требует официальная инструкция, затем вызываю интерактивное восстановление и выбираю одну точку:
cd /opt/mailcow-dockerized
MAILCOW_BACKUP_LOCATION=/mnt/restore/mailcow THREADS=2 \
./helper-scripts/backup_and_restore.sh restoreДля полноценного сервиса выбираю all; для исследования архива обязательны хотя бы vmail и crypt. Отдельно сверяю MAILDIR_SUB: несовпадение со старой установкой способно спрятать письма даже при исправных данных и ключах.
Одно письмо проверяю штатным doveadm fs get. Команда выполняется внутри Dovecot и последовательно снимает шифрование и LZ4-сжатие:
cd /opt/mailcow-dockerized
docker compose exec dovecot-mailcow /bin/bash
file=/var/vmail/example.com/user/Maildir/cur/NAME-OF-MESSAGE
doveadm fs get \
compress lz4:1:crypt:private_key_path=/mail_crypt/ecprivkey.pem:public_key_path=/mail_crypt/ecpubkey.pem:posix:prefix=/ \
"$file" > /tmp/message.eml
head -n 5 /tmp/message.emlВ результате должны появиться обычные заголовки письма: Return-Path, Received, From или другие RFC-поля. Если команда сообщает об отсутствии закрытого ключа или ошибке расшифровки, проблема не лечится распаковкой LZ4 отдельно.
Для массовой выдачи архива я не запускаю опубликованный mailcow цикл расшифровки на единственной копии: пример в документации заменяет исходные файлы и прямо помечен как операция на свой риск. Сначала клонирую восстановленный том или создаю снимок, затем расшифровываю копию и проверяю количество файлов, размеры и выборочные вложения. Синтаксис doveadm fs get из документации mailcow и разделение компонентов vmail/crypt в скрипте я сверяю с актуальной веткой перед каждым проектом. Мой приоритет прост: сначала сохранить исходный закрытый ключ, затем доказать чтение одного письма и только после этого автоматизировать весь экспорт.
- Для возврата mailcow в работу — восстановить компоненты через штатный скрипт.
- Для единичной экспертизы — использовать `doveadm fs get` внутри Dovecot.
- Для миграции — работать с клоном и получить обычный Maildir или передать письма через IMAP/dsync.
- После экспорта — проверить число писем, MIME-вложения и права владельца, а не только размер каталогов.
Частые вопросы
Достаточно ли сохранить только ecprivkey.pem?
Закрытый ключ содержит критически важный материал, а открытый ключ технически можно получить из него. Но я сохраняю оба исходных файла — `ecprivkey.pem` и `ecpubkey.pem`. Это исключает лишние преобразования и позволяет сразу проверить соответствие пары.
Включает ли backup vmail том crypt-vol-1?
Нет. В актуальном `backup_and_restore.sh` это разные компоненты. Используйте `backup vmail crypt`, добавьте `mysql` для восстановления учётных данных либо выбирайте `backup all`.
Можно ли открыть зашифрованный файл в Thunderbird?
Напрямую нельзя. Сначала его нужно обработать Dovecot с исходным закрытым ключом и LZ4-декодированием, например через `doveadm fs get`. Получившийся `.eml` уже открывается обычным почтовым клиентом.
Поможет ли сброс пароля почтового ящика?
Нет. Пароль пользователя и глобальный закрытый ключ mail-crypt — разные сущности. Смена пароля IMAP не восстанавливает утраченный `ecprivkey.pem`.
Нужно ли сохранять vmail-index-vol-1?
Для расшифровки и чтения писем — нет, индексы можно перестроить. Если нужен быстрый запуск большой восстановленной системы, их копия иногда экономит время, но я не ставлю её выше `vmail`, `crypt` и `mysql`.
Что ещё кроме vmail и crypt нужно для полного восстановления?
`mysql` — домены, ящики, алиасы и квоты; `redis` — в том числе DKIM-ключи; `rspamd` — обучение антиспама; `postfix` — очередь. Проще всего выбирать `all` и отдельно копировать каталог `/opt/mailcow-dockerized`.
Источники
- Mail crypt — mailcow documentation — Раздел Mail crypt: сжатие LZ4, шифрование, расположение пары ключей и пример `doveadm fs get`. https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-mail-crypt/
- Backup — mailcow documentation — Разделы Manual, Variables for backup/restore script и Cronjob: выбор компонентов `vmail`, `crypt`, `mysql`, `all`, переменные `THREADS` и `MAILCOW_BACKUP_LOCATION`. https://docs.mailcow.email/backup_restore/b_n_r-backup/
- Restore — mailcow documentation — Раздел Restoring Data: подготовка пустой системы, выбор Crypt data и Mail directory, предупреждение о `MAILDIR_SUB`. https://docs.mailcow.email/backup_restore/b_n_r-restore/
- backup_and_restore.sh — mailcow-dockerized — Исходный скрипт (ветка master): компоненты `vmail|crypt|redis|rspamd|postfix|mysql|all`, архивы `backup_vmail.tar.zst` и `backup_crypt.tar.zst`, копирование `mailcow.conf`, обработка `--delete-days`. https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
- docker-compose.yml — mailcow-dockerized — Сервис dovecot-mailcow (ветка master): `vmail-vol-1:/var/vmail`, `vmail-index-vol-1:/var/vmail_index`, `crypt-vol-1:/mail_crypt/`. https://github.com/mailcow/mailcow-dockerized/blob/master/docker-compose.yml
- dovecot.conf — mailcow-dockerized — Штатная конфигурация Dovecot: `mail_crypt_global_private_key`, `mail_crypt_global_public_key`, `mail_crypt_save_version = 2`, `zlib_save = lz4`. https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/dovecot.conf
- mail-crypt-plugin — Dovecot 2.3 documentation — Разделы Elliptic Curve Key, Decrypting Files и fs-crypt: роль закрытого и открытого ключей, порядок сжатия и шифрования, синтаксис `doveadm fs get/put`. https://doc.dovecot.org/2.3/configuration_manual/mail_crypt_plugin/
