АйТи Фреш
Главная / Статьи / Серверы и хостинг
Серверы и хостинг

Почему копии vmail недостаточно для чтения архива mailcow

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~14 мин чтения
Почему копии vmail недостаточно для чтения архива mailcow
Иллюстрация к статье «Почему копии 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; открытый ключ из него технически можно получить заново, однако я всегда сохраняю исходную пару целиком. Стоимость — несколько килобайт. Цена ошибки — весь архив.

Если исходный `ecprivkey.pem` потерян и другой копии ключа нет, новый ключ не поможет. Он сможет шифровать новые письма, но не расшифрует уже сохранённые объекты.
Памятка: Maildir есть, но письма в нём не обычные — схема
Памятка: Maildir есть, но письма в нём не обычные. Открыть схему в полном размере

Какие данные я считаю полноценным архивом

Для чтения отдельных писем минимальный комплект состоит из 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: получив оба компонента, злоумышленник получает возможность читать почту. Я держу рабочий набор в зашифрованном хранилище резервных копий, а контрольную копию пары ключей — в отдельном защищённом хранилище с ограниченным доступом. Это спорный компромисс между доступностью и секретностью, но потерять единственный ключ из-за отказа одного массива гораздо реальнее, чем кажется.

Копия считается резервной только после тестового чтения письма. Наличие каталога, нормальный размер архива и нулевой код завершения `rsync` не проверяют пригодность закрытого ключа.
Почему копии vmail недостаточно для чтения архива mailcow — схема
Схема к статье. Открыть схему в полном размере

Как за пять минут поставить диагноз

Сначала я ничего не расшифровываю и не изменяю. Проверяю, какой том подключён к 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
'

Совпавшие хеши подтверждают пару, но ещё не доказывают, что именно ею зашифрован архив. Окончательный тест — расшифровать одно старое и одно свежее письмо. Если новое читается, а старое нет, я ищу замену ключей или смешение поколений резервных копий.

Не выводите содержимое `ecprivkey.pem` в терминал, тикет или журнал CI. Для проверки достаточно размера файла, разбора ключа через OpenSSL и тестовой расшифровки.

Как я строю резервное копирование 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
Не копируйте `backup_and_restore.sh` в отдельный каталог. Официальная документация требует запускать скрипт из дерева mailcow: он использует соседние конфигурационные файлы и данные проекта.
Порядок действий: Как я строю резервное копирование mailcow — схема
Порядок действий: Как я строю резервное копирование 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 ГиБ копии за пять лет аудиторской переписки были бы практически бесполезны.

Название компании и параметры проекта приведены как условный практический сценарий и не раскрывают сведения о конкретном заказчике.

Как восстановить письмо или сделать переносимый 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 в скрипте я сверяю с актуальной веткой перед каждым проектом. Мой приоритет прост: сначала сохранить исходный закрытый ключ, затем доказать чтение одного письма и только после этого автоматизировать весь экспорт.

Не расшифровывайте рабочий `/var/vmail` «на месте» ради удобного архива. Ошибка пути, нехватка места или прерванный цикл могут смешать зашифрованные и открытые объекты. Работайте со снимком или отдельной восстановленной копией.
Порядок действий: Как восстановить письмо или сделать переносимый Maildir — схема
Порядок действий: Как восстановить письмо или сделать переносимый Maildir. Открыть схему в полном размере

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

Достаточно ли сохранить только 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`.

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

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

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

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

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

Источники

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