Смена пароля PostgreSQL без простоя: что на самом деле делает ALTER USER
Короткий ответ: ALTER USER … WITH PASSWORD не рвёт ни одного открытого
соединения. PostgreSQL проверяет пароль один раз — когда соединение
устанавливается, — и уже работающие сессии продолжают работать со старым
паролем сколько угодно долго. Простой возникает не в момент смены, а позже:
на первом же переподключении сервиса, который читает пароль из старого
.env, из переменной окружения или из пула, поднятого час назад.
Отсюда практический вывод: опасна не сама смена пароля, а окно между сменой и тем моментом, когда все потребители узнали новое значение. Ниже — как это окно измерить, чем его сократить до нуля и что для этого должно быть готово в самой базе.
Что происходит в базе
Запрос ровно один:
ALTER USER app_a WITH PASSWORD 'new-password';
Что делает сервер:
- сохраняет хэш пароля, а не сам пароль: с PostgreSQL 14 по умолчанию
scram-sha-256(настройкаpassword_encryption); - ничего не сообщает открытым сессиям. Они уже прошли аутентификацию, и повторно её никто не запрашивает;
- начинает требовать новый пароль от всех новых подключений — немедленно.
Побочный эффект, о котором забывают: при log_statement = 'ddl' или 'all'
текст запроса попадает в лог сервера вместе с паролем. Если вы меняете
пароли скриптом, проверьте эту настройку, прежде чем скрипт отработает первый
раз.
Где на самом деле возникает простой
Соединения не рвутся — значит, ничего не ломается сразу. Это и есть ловушка: поломка отложена и приходит в неудобный момент.
| Что происходит | Когда сломается |
|---|---|
| Работающие сессии приложения | никогда — до конца их жизни |
| Пул соединений (pgbouncer, HikariCP, pgx) | при добавлении соединения в пул: под нагрузкой, а не сразу |
| Раскатка новой версии сервиса | при старте пода с прежним значением в переменной окружения |
| Ночной cron-скрипт | ночью |
| Реплика, читающая через тот же логин | при сетевом разрыве и переподключении |
Иными словами: смена пароля вручную выглядит успешной ровно до первого переподключения — и именно поэтому её последствия обычно разбирают не тот человек и не в тот час, который её сделал.
Почему ручной сценарий рвётся
Классический порядок — «сменить пароль в базе, потом разнести по сервисам» — создаёт окно, в котором часть инстансов уже читает новое значение, а часть ещё старое. Одно из двух обязано не работать: если пароль в базе уже новый, то каждое новое подключение со старым значением отклоняется.
Поменять порядок нельзя: разнести новый пароль до того, как он заведён в базе, — это то же окно, только зеркальное. Единственный способ убрать окно — сделать так, чтобы момент «в базе новый пароль» и момент «потребители узнали новый пароль» не совпадали по требованию к синхронности: потребители должны до последнего читать старое значение, а переключиться — атомарно и только после того, как новое проверено.
Четыре фазы вместо одного вечера
Автоматическая ротация в env-master разрезает замену на четыре шага:
create → set → test → finish
- create — сгенерировать новый пароль. Базу эта фаза не трогает вовсе;
- set — выполнить
ALTER USERпод административным подключением; - test — подключиться новым логином и паролем и выполнить
SELECT 1; - finish — перенести метку
current, после чего потребители начинают получать новое значение.
Ключевое здесь — третья фаза. Ротация без проверки хуже её отсутствия: вы
получаете уверенность вместо работающего доступа. Пароль сменён, разослан — и
не подходит, потому что у роли нет CONNECT на нужную базу или она не
LOGIN. Если test не прошёл, метка current не двигается: потребители
продолжают работать со старым значением, а администратор получает событие
rotation.failed, вебхук и метрику вместо тихого отказа под нагрузкой.
Между create и finish новый пароль уже сохранён под меткой pending. Это
не деталь реализации: если процесс упадёт после ALTER USER, но до записи в
хранилище, пароль будет потерян — а он уже действует в базе, и восстановить
его неоткуда.
Один пользователь или два
Две стратегии отличаются только тем, чей пароль меняется за прогон.
single-user — меняется пароль той единственной роли, под которой ходят
сервисы. Открытые соединения живут, новые подключения со старым паролем
отклоняются, пока сервис не перечитает значение. С пулом соединений это
обычно проходит незаметно: пул держит соединения открытыми, а новые
поднимаются уже с новым паролем.
dual-credential — в базе заведены две равноправные роли, и прогон меняет
пароль той, что сейчас не используется. Схема задумана как «нулевой простой»:
пока одно имя ротируется, второе обслуживает соединения.
Чтобы вторая схема работала, менять надо не пароль, а пару целиком: активная
роль меняется каждый прогон, и пароль без имени сервису бесполезен. Поэтому
dual-credential публикует строку подключения:
postgres://app_b:pg_xxxxxxxx@db.internal:5432/billing
Имя и пароль приезжают вместе, одной версией одной записи — то есть атомарно.
Между двумя отдельными записями всегда есть промежуток, в котором они не
совпадают, и попасть в него — значит получить пароль одной роли с именем
другой. Записи с такой ротацией и называть стоит соответственно:
DATABASE_URL, а не DB_PASSWORD.
Что подготовить в базе
Ротатор меняет пароли, но не создаёт роли и не раздаёт права. До первого прогона в базе должно быть:
CREATE ROLE app_a LOGIN PASSWORD 'temporary';
GRANT CONNECT ON DATABASE billing TO app_a;
-- и ваши гранты на схемы и таблицы
Административное подключение, под которым выполняется ALTER USER, — тоже
секрет, и хранить его надо как секрет: отдельной записью с видимостью
«только сервисам», а не строкой в конфиге политики. Суперпользователь не
обязателен: достаточно роли, которой в вашей версии PostgreSQL позволено
менять пароль именно этим пользователям.
Как сервис узнаёт новое значение
Смена пароля бесполезна, если сервис читает его один раз при старте. Порядок, при котором окно действительно закрывается:
- сервис берёт значение через SDK, а не из переменной окружения;
- SDK держит записи в кэше — по умолчанию 15 минут, поэтому обычная задержка
после
finishне превышает четверти часа; refresh()сбрасывает кэш немедленно; вызов по вебхукуentry.rotatedпревращает ротацию в событие для сервиса, а не в ожидание;- драйвер базы получает пароль в момент установки соединения — то есть при следующем добавлении соединения в пул, а не при перезапуске процесса.
Чек-лист
- пароль меняется командой, которая не рвёт открытые соединения — проверьте, что вы не перезапускаете базу «чтобы применилось»;
- новый пароль проверен подключением до того, как о нём узнали потребители;
- потребители читают значение из хранилища, а не из образа и не из
.env; - административный DSN лежит как секрет и не виден людям;
- у ротации есть расписание, а не повод: секрет, который меняют только после инцидента, к моменту инцидента живёт уже несколько лет.
Как это настроить за пять шагов — в документации по ротации. Почему секреты вообще не стоит доставлять файлом — в статье «Почему .env-файлам не место в продакшене».