Воскресенье, двенадцать сорок. На столе распечатка хронологии: полтора десятка строк, время по UTC в левой колонке, до минуты. Читаю сверху вниз, как расписание электричек, с той разницей, что одна остановка здесь длилась сорок семь минут.

Принтер кончился на предпоследней странице, последнюю печатала на обороте старой квитанции. Сторона не та, зато время в левой колонке сходится.

Это была не авария

Прод переставили заново не потому, что он сломался. Причина хуже: к машине, на которой собирались его образы, больше не было доверия, а чистка по месту такого доверия не возвращает. Решение — чистая система на том же сервере: адреса, домены и файлы в хранилище остаются, меняется всё, что под ними.

В процедуре с первой страницы записан принцип, из которого вырос весь порядок работ: сервер не стирается, пока не пройдены ворота, и до этого момента старый прод поднимается обратно одной командой. Оттуда же деление ролей. Всё в терминале делает агент с доступом; человек принимает решения — говорит «останавливай прод», переименовывает базу и меняет ключи в кабинетах провайдеров. Переименование — одна строка, и её вставляет человек: гард не даёт агенту восстанавливать базу, если в её имени нет слова canary.

Сорок семь минут по строкам

В пять ноль семь по UTC остановлены таймеры и nginx, активных задач — ноль. Финальный бэкап с контрольной суммой, четыре минуты на перенос, девяносто секунд на пробное восстановление здесь же, на ноутбуке. Сошлось со снимком, снятым днём раньше: сто пятнадцать пользователей, девятьсот восемьдесят четыре проекта, тридцать девять тысяч пятьсот восемьдесят шесть задач, сто тридцать девять таблиц, сто двадцать одна из них с правилами доступа на уровне строк, последняя миграция — 414.

В пять тридцать пять человек переустановил систему. За следующие пятнадцать минут — система, Docker, nginx, сертификаты, файрвол, образы, код, конфигурация и восстановление базы уже на сервере: двести тридцать три секунды.

В пять пятьдесят две переименована база, в пять пятьдесят четыре — выкатка. Второй попытки потребовали заглушки вместо образов MinIO, которых в реестрах больше нет; с nginx сайт открылся. Сорок семь минут.

Через три минуты тестовая генерация через коннектор прошла до конца. К шести ноль пяти вернулся сборщик ошибок вместе со своей базой: ключ к нему запечён в образ, и без базы ошибки начали бы теряться молча — то есть снаружи всё выглядело бы прекрасно.

Инфографика: пять фаз переустановки прода и их длительность в минутах — финальный бэкап 10, дамп и пробное восстановление 16, переустановка системы 4, система и восстановление базы 11, переименование базы и выкатка 4; между фазами паузы на решения владельца
Хронология из записи прогона, 31 октября 2026, время UTC. Сумма фаз — сорок пять минут из сорока семи: остальное ушло на паузы и решения, которые принимал человек.

Три поломки одного класса

Самая скучная часть — смена ключей — оказалась самой дорогой. Тридцать первого октября их было три, и все три одного класса: ключ жил в нескольких местах, а меняли одно.

Симптомы при этом разные, и в этом всё дело. В одном случае значение обновили в файле, а читал его контейнер со старым. В другом новый пароль базы «подходил», хотя был сменён: внутри контейнера вход по локальному адресу доверенный, и проверять надо по сети. Отсюда правило, которое дороже любого каталога: проверять ключ изнутри того, кто им пользуется, а не с машины, на которой его поменяли.

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

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

Что осталось после прогона

Правка 2163 добавила три вещи и не удалила ни строки: 1243 добавленные, пятнадцать файлов, от открытия до слияния два часа сорок восемь минут.

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

Запись прогона: хронология, расхождения с планом, итог по каждому ключу. Расхождения перечислю, потому что именно они делают документ честным, а не отчётным. Архивация с шаблонами теряла юниты. Архив, собранный на маке, приносил служебные файлы, которые nginx потом подключал как сайты. Образов MinIO в реестрах больше нет, на их месте заглушки. Клон приватного репозитория с сервера требует токена, поэтому файл данных положили руками. Автовакуум мешал переименованию базы.

И скрипты: развёртывание, укрепление, приём ключей со стандартного ввода с проверкой у провайдера до записи. Значений секретов в файлах нет — это проверено правилом поиска секретов, отдельно от проверки синтаксиса.

Цена этой правки

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

Что проверили и написали об этом: синтаксис и shellcheck по всем скриптам, правило поиска секретов по всем новым файлам — ноль находок, с оговоркой, что на заведомой утечке правило срабатывает. И честная строка про то, чего проверить не вышло: регрессионный прогон сканера локально не запускается из-за неполного клона, его сделает CI, когда Actions вернётся.

Actions не вернулся. Последний запуск создан тридцатого октября в 08:30:03 UTC и упал на старте; к полудню первого ноября это сорок девять часов тишины. А в предполётной проверке переустановки с этой недели уже есть пункт «идёт ли Actions» — то есть двухдневный отказ проверок был замечен пунктом чек-листа, а не событием.

Сорок семь минут простоя — самая дешёвая часть этой истории. Дорого стоило другое: три отказа, у которых одна причина и три разных симптома, и то, что восстанавливать пришлось не машину, а список мест, где живёт один и тот же ключ.

В каталоге есть строка про пароль базы, который «подходит», хотя сменён: внутри контейнера вход доверенный, проверять надо по сети. Тридцать таких строк. Это и есть отчёт о прогоне, который стоит читать.