Что случилось
В апреле я делал техдолг на собственном VPS, чистил неиспользуемые пакеты Dovecot. Команда apt purge dovecot-sieve dovecot-managesieved казалась безопасной. Два маленьких пакета фильтрации, которыми я не пользуюсь. Через несколько секунд Dovecot перестал слушать порты. Через 12 минут я вернул почту из ночного бэкапа.
Это RCA, который я устроил себе сам. Пост для всех, кто думает, что apt purge без --dry-run на проде сойдёт с рук. И для CIO компании среднего размера, у которой схожий боевой стек на Debian.
Контекст
VPS у Timeweb, Debian 13, multi-tenant. На нём крутится почта и веб моих личных и клиентских проектов. Я веду этот сервер сам, потому что не хочу терять hands-on. Платформа для меня сразу и обучение, и боевая инфраструктура.
Почтовый стек на момент инцидента: Postfix 3.10, Dovecot 2.4, Rspamd, Roundcube. Бэкап ежедневный, в 03:00 ночи, через собственный full_backup.sh по таймеру systemd. Бэкапятся /etc, /var/vmail и MariaDB-дамп.
Я зашёл на сервер в 02:30, чтобы закрыть пункт техдолга. Убрать неиспользуемые пакеты Dovecot. Sieve-фильтры на этом сервере я не использовал, вся фильтрация спама на стороне Rspamd. Логика была простая. Нет пакетов, значит нет лишней поверхности атаки и нет лишних пакетов в апдейтах.
Команда, которая всё сломала
bashapt purge dovecot-sieve dovecot-managesieved
Здесь зарыта вся история. Я набрал команду на автопилоте, как обычно чищу пакеты. По логике это два маленьких пакета sieve, которые мне не нужны. Снести их и забыть. Я был уверен, что dovecot-sieve это изолированный аддон поверх Dovecot, и его удаление ничего не заденет. Зависимость я держал в голове задом наперёд.
На Dovecot 2.4 в Debian 13 цепочка идёт не от аддона к ядру, а наоборот. dovecot-core объявляет Depends: dovecot-sieve. Значит, sieve для core это обязательная зависимость, а не опция. Проверить легко:
bashapt-cache depends dovecot-core | grep -i sieve # Depends: dovecot-sieve
Из-за этого нельзя выкинуть sieve, не выкинув core, который от sieve зависит. APT увидел запрос на удаление dovecot-sieve, поднял обратную цепочку и понял, что dovecot-core без него существовать не может. А раз уходит core, за ним каскадом уходят dovecot-imapd, dovecot-lmtpd и dovecot-mysql, потому что они держатся на core. Голый purge без всякого autoremove снёс шесть пакетов вместо двух названных.
Дальше сработал сам режим purge. Он сносит бинарники пакета вместе с его conffiles. У dovecot-core conffile это /etc/dovecot/dovecot.conf, главный конфиг почты. Раз core попал в транзакцию через Depends-цепочку, purge удалил его конфиг с диска целиком. Не сбросил на дефолт, а стёр файл.
Подтверждение, которое APT мне показал в терминале, я прокликал бегло. Глаз поймал короткий список sieve. А на самом деле APT выкатил «The following packages will be REMOVED» длиной в шесть строк, и в этих строках стояли dovecot-core, dovecot-imapd, dovecot-lmtpd, dovecot-mysql. Нажал Y.
Полная хронология
| Время | Событие | Источник |
|---|---|---|
| 02:48:50 | Запустил apt purge -y dovecot-sieve dovecot-managesieved. Нажал Y без вдумчивого чтения списка. | apt history.log |
| 02:49:00 | APT удалил шесть пакетов: dovecot-core, dovecot-imapd, dovecot-lmtpd, dovecot-mysql, dovecot-sieve, dovecot-managesieved. /etc/dovecot/dovecot.conf стёрт с диска (purge снёс conffile core). Dovecot перестал слушать 143/993/110/995. | apt history.log + journalctl |
| 02:49:10 | Прочитал /var/log/apt/history.log. Увидел в строке Purge все шесть пакетов, не два. Понял масштаб. Решил восстанавливаться из daily-backup от 03:00 предыдущей ночи. RPO 24 часа, для личного проекта допустимо. | apt history.log |
| 02:49:30 | apt install dovecot-core dovecot-imapd dovecot-lmtpd. Пакеты вернулись (dovecot-sieve подтянулся как automatic, он же обязательная зависимость core). Лёг дефолтный dovecot.conf. | apt history.log |
| 02:52 | Распаковал /root/backups/site_2026-04-23_0300.tar.gz в /tmp/restore/. Заметил, что dovecot-mysql не вернулся. Без него Dovecot не достучится до виртуальных ящиков в MariaDB. | tar log |
| 02:53:38 | apt install dovecot-mysql. Отдельным шагом вернул драйвер userdb/passdb для MariaDB. | apt history.log |
| 02:58 | Скопировал /etc/dovecot/ из бэкапа поверх дефолтного. Вернул свой рабочий конфиг. | tar log |
| 02:59 | doveconf -n показал, что конфиг валиден, без синтаксических ошибок. systemctl start dovecot. | journalctl |
| 03:00 | В журналах вижу, что dovecot слушает 143/993, MySQL-userdb отвечает. Открыл Mail.app, почта пришла. | Mail.app |
| 03:00:50 | Полная функциональность восстановлена. Время восстановления составило 12 минут от точки отказа. | — |
Восстановление по шагам
bash# Шаг 1. Распаковка бэкапа в безопасное место mkdir -p /tmp/restore cd /tmp/restore tar -xzf /root/backups/site_2026-04-23_0300.tar.gz # Шаг 2. Переустановка пакетов (поставит дефолтный dovecot.conf) apt install -y dovecot-core dovecot-imapd dovecot-lmtpd # Драйвер MySQL отдельным пакетом — без него userdb/passdb в MariaDB не работают apt install -y dovecot-mysql # Шаг 3. Свой конфиг из бэкапа — ПОВЕРХ дефолтного cp -r /tmp/restore/etc/dovecot/ /etc/ # Шаг 4. Проверка конфига перед стартом doveconf -n >/dev/null || { echo "Config broken, abort"; exit 1; } # Шаг 5. Старт сервиса systemctl start dovecot systemctl status dovecot # Шаг 6. Smoke test — IMAP-логин openssl s_client -connect localhost:993 -quiet <<< "A001 LOGIN user pass"
Шаг 2 пришлось делать в два захода. Первым apt install dovecot-core dovecot-imapd dovecot-lmtpd вернул бинарники и дефолтный dovecot.conf, но не вернул dovecot-mysql. А он на этом сервере критичен. Логины и пароли виртуальных ящиков лежат в MariaDB, и без MySQL-драйвера Dovecot не прочитает userdb и passdb. Запусти я сервис после первого install, порты бы открылись, но ни один реальный ящик не залогинился бы. Драйвер я доставил отдельным apt install dovecot-mysql.
Порядок здесь не косметика. Сначала apt install, он ставит бинарники и дефолтный конфиг. Только потом конфиг из бэкапа кладётся поверх дефолтного. Если сделать наоборот, положить конфиг до установки пакетов, dpkg упрётся в уже лежащий файл и либо задаст conffile-вопрос, либо перетрёт восстановленный конфиг дефолтным.
Шаг 4 критичный. Если бы я просто запустил dovecot без doveconf -n, мог получить вторичный фейл. Конфиг из бэкапа лёг поверх свежепереустановленных пакетов, а минорная версия из репозитория могла оказаться новее той, при которой конфиг писался. На смене минора синтаксис местами уезжает. doveconf -n разбирает весь конфиг и падает с ненулевым кодом, если в нём есть незнакомый или сломанный параметр. К счастью, расхождений не было.
RCA. Почему так получилось
Корневая причина. Я выполнил apt purge на проде без предварительного --dry-run и без снапшота conffiles.
Фактор 1. Усталость. 02:48 ночи. Концентрация ниже дневной. APT-подтверждение я прокликал бегло.
Фактор 2. Зависимость задом наперёд. Я считал, что dovecot-sieve это изолированный аддон поверх ядра, и его снос ничего не заденет. На деле на Dovecot 2.4 в Debian 13 всё ровно наоборот. dovecot-core объявляет Depends: dovecot-sieve, то есть это ядро держится на sieve, а не sieve на ядре. Удалить sieve, не удалив core, нельзя по определению зависимости. А раз уходит core, каскадом уходят imapd, lmtpd и mysql, которые висят на нём. Тут не нужен ни autoremove, ни какой-то лишний флаг. Прямой purge двух пакетов утащил шесть просто из-за Depends-цепочки. Команда сделала ровно то, что я ей сказал. Я неверно представлял, что именно говорю.
Отдельно отмечу нюанс, чтобы не упрощать. Если на хосте включён глобальный APT::Get::AutomaticRemove, даже точечный purge может добрать сирот сверх Depends-цепочки. На моём сервере этот ключ пуст (проверил через apt-config dump | grep AutomaticRemove), так что сносить почту автоудалению было не нужно. Хватило прямой обязательной зависимости core → sieve.
Фактор 3. Нет процесса. На своём VPS я работал без чек-листа на деструктивные операции. На корпоративных серверах в моей практике такой чек-лист есть и обязателен. Дома почему-то нет.
Если бы я делал ту же операцию на проде клиента, чек-лист бы сработал и инцидента не было бы. То, что я наступил на эти грабли на своём VPS, а не на клиентском, в каком-то смысле повезло. Урок обошёлся в 12 минут моей почты, а не в доверие клиента.
3 шага, которые не дали бы этому случиться
Шаг 1. apt-get purge --dry-run
Запускать любую apt purge/remove в режиме сухого прогона до боевого выполнения.
bashapt-get purge --dry-run dovecot-sieve dovecot-managesieved
В выводе ищем секцию The following packages will be REMOVED:. Если там перечислены только целевые пакеты, идём дальше. Если APT по зависимостям тянет что-то ещё (особенно -core, -common, любые системные dependencies), стоп, отмена. Ищем другой способ. Отключаем функционал через config или через systemctl disable, оставив пакет установленным.
Мой инцидент целиком в этом одном шаге. Тот же apt purge dovecot-sieve dovecot-managesieved в режиме --dry-run вывел бы шесть строк в REMOVED, и dovecot-core в их числе. Тридцать секунд чтения, и я бы отменил операцию, а не нажал Y вслепую. Дры-ран запускаем с тем же набором пакетов и флагов, что и боевую команду. Он показывает итоговый план транзакции ровно так, как APT его исполнит.
Шаг 2. Снапшот conffiles перед операцией
Даже если --dry-run показал чистый список, делаем tar-снапшот всех conffiles затронутых пакетов:
bash# Какие conffiles будут потеряны при purge: dpkg-query -W -f='${Conffiles}\n' dovecot-* | awk '{print $1}' # Tar их заранее: tar -czf /root/backups/dovecot-conffiles-$(date +%F-%H%M).tar.gz \ $(dpkg-query -W -f='${Conffiles}\n' dovecot-* | awk '{print $1}')
Снапшот делается за 2–3 секунды и весит обычно меньше 100 КБ. Это страховка на случай, если --dry-run чего-то не показал.
Шаг 3. Постфактум smoke test
После любой деструктивной операции обязателен smoke test критичных функций. Для почты это:
bash# Слушает ли Dovecot ss -tlnp | grep -E ':(143|993|110|995)\b' # Принимает ли логин (через SSL) openssl s_client -connect localhost:993 -quiet <<< 'A1 LOGIN test bad' # Postfix → Dovecot LMTP-доставка echo "Subject: test" | sendmail me@example.ru tail -f /var/log/mail.log # ждём delivery line
На smoke test уходит 30–60 секунд. На восстановление из бэкапа уйдёт 12 минут плюс ночное пробуждение.
Что записал в свои правила
На моём ноутбуке для каждого проекта есть файл правил CLAUDE.md. Я веду его около двух лет для работы с AI-ассистентом (Claude Code). Туда вношу всё, что узнаю на собственной шкуре. После этого инцидента в файл добавился такой блок:
Правило помогает не мне одному. Я отдаю этот же файл командам клиентов, у которых внедряю AI-ассистент в инженерные процессы. Ассистент с таким правилом не запустит apt purge на проде без --dry-run. А если конкретный инженер про протокол забыл, ассистент напомнит за него.
Для кого это важно
Если вы CIO компании с production-сервером на Debian/Ubuntu, этот класс рисков актуален и для вас. Любая команда apt на хосте под нагрузкой может стоить минут даунтайма. Особенно на ночных операциях, когда дежурный инженер устал и отвлекается на телефон и чаты.
Если у вас vCIO, который снимает с вас этот класс рисков, у вас должна быть уверенность, что подобный протокол у него прописан. Я отдаю свой CLAUDE.md любому клиенту, с которым работаю. Целиком, без купюр. Это больше тысячи строк правил, выработанных за два года практики.
Если хотите видеть больше скучных инфраструктурных разборов с реальными командами и реальным ущербом, посмотрите остальные записи журнала. Про методологию аудита я писал в записи про карту ИТ-зрелости.
Я работаю с этим напрямую — ознакомительный звонок 30 минут
Расскажете ситуацию — отвечу за 4 часа в рабочее время. Звонок бесплатный, без обязательств. Если задача не моя — порекомендую коллег, у которых она хорошо ляжет.