env-master
ENСоздать аккаунт
Разделы документации

Ротация секретов

Замена значения без простоя и без ручной работы: env-master сам меняет пароль в целевой системе, проверяет его и только после проверки показывает сервисам.

Ротация включается политикой на запись: одна запись — одна политика. Пока политики нет, значение живёт столько, сколько его туда положили.

Четыре фазы

create → set → test → finish
  • create — получить новое значение (у postgres — сгенерировать пароль; пользователи из usernames должны существовать в базе заранее);
  • set — применить его там, где оно должно действовать;
  • test — убедиться, что оно работает;
  • finish — перенести метку current, чтобы потребители начали читать новое.

Прогон всегда идёт с начала. Фоновый воркер тикает каждые 30 секунд и захватывает политику арендой на 2 минуты, так что два прогона одной политики не пересекаются: ручной запуск берёт ту же аренду, что и воркер.

Возобновления прогона с фазы нет и не планируется: прерванный прогон никогда не продолжается, следующий всегда начинается заново с create. Для postgres возобновление ничего не экономило бы — его create не трогает базу, а только генерирует пароль, поэтому «заново» стоит один лишний ALTER USER тому же пользователю. Ротатору, у которого create создаёт что-то с лимитом (например, ключи доступа с ограничением на пользователя), возобновление придётся проектировать заново вместе с самим ротатором.

Что именно делает ротатор postgres

Единственный встроенный ротатор — postgres. По фазам, без обобщений:

ФазаЧто происходит в вашей базе
createбазу не трогает. Генерируется пароль pg_ + 24 случайных байта; для формы «строка подключения» читается запись с админским DSN, чтобы взять из неё хост, порт и базу
setподключение под админским DSN и один запрос: ALTER USER "app_b" WITH PASSWORD '…'
testподключение тем самым значением, которое получит сервис, и запрос SELECT 1
finishничего в базе. Внутри env-master метка current переносится на новое значение, а в конфиг политики записывается имя пользователя, ставшего активным

Отсюда три следствия, которые экономят вечер отладки:

  1. Ротатор не создаёт пользователей. Роли из usernames вы заводите сами и сами выдаёте им гранты. Ротатор только меняет им пароль.
  2. Админский DSN должен быть URL — postgres://admin:pw@host:5432/db. Фаза test подставляет в него ротируемого пользователя и новый пароль, а для этого разбирает его как адрес; формат host=… user=… на этой фазе не подойдёт.
  3. Ротируемый пользователь должен подключаться туда же, куда админ. Хост, порт, база и параметры для test берутся из админского DSN — меняются только имя и пароль. Если у роли нет права CONNECT на эту базу, прогон упадёт на test, хотя пароль уже сменён.

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

Настройка: пять шагов

Продолжение быстрого старта: переменные BASE, TOKEN, PROJECT и ORG — оттуда.

1. Заведите роли в PostgreSQL

CREATE ROLE app_a LOGIN PASSWORD 'temporary-a';
CREATE ROLE app_b LOGIN PASSWORD 'temporary-b';
GRANT CONNECT ON DATABASE billing TO app_a, app_b;
-- дальше — ваши гранты на схемы и таблицы, одинаковые для обеих ролей

Пароли здесь временные: первый же прогон заменит их своими.

2. Положите админский DSN отдельной записью

curl -s "$BASE/projects/$PROJECT/entries?env=production" \
  -X PUT -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"key":"DB_ADMIN_DSN","kind":"secret","visibility":"restricted",
       "value":"postgres://rotator:pw@db.internal:5432/billing"}'

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

3. Заведите запись, которую будете ротировать

ENTRY=$(curl -s "$BASE/projects/$PROJECT/entries?env=production" \
  -X PUT -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"key":"DB_PASSWORD","kind":"secret","visibility":"masked","value":"s3cret"}' \
  | jq -r .id)

Тип обязательно secret. У переменной (var) значение хранится открытым текстом — сгенерированный пароль оказался бы виден всем, кто видит таблицу.

4. Включите политику

single-user в примере — не потому, что dual-credential не работает (работает, см. «Стратегии»), а потому, что для первого прогона проще: одна роль, одно значение, меньше движущихся частей.

POLICY=$(curl -s $BASE/entries/$ENTRY/rotation-policy \
  -X PUT -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
    "rotator": "postgres",
    "strategy": "single-user",
    "intervalSeconds": 604800,
    "config": {
      "adminDsnEntryRef": "my-org/billing-api/production/DB_ADMIN_DSN",
      "usernames": ["app_a"]
    }
  }' | jq -r .id)

adminDsnEntryRef — ссылка орг/проект/окружение/КЛЮЧ, а не значение: DSN остаётся записью со своими правами и своим журналом доступа. Ссылка обязана вести в ту же организацию, и на её проект у вас должна быть роль не ниже member — иначе политику не сохранить.

5. Проверьте первым прогоном, не дожидаясь расписания

curl -s $BASE/rotation-policies/$POLICY/run-now \
  -X POST -H "authorization: Bearer $TOKEN" | jq '.status, .phase, .error'

Ждать неделю, чтобы узнать, что в DSN опечатка, незачем. Что означают ошибки первого прогона — в разделе «Когда прогон падает» ниже.

Стратегии: что выбрать

single-userdual-credential
Пользователей в usernamesодиндва, разных
Что меняется за прогонпароль этого пользователяпароль неактивного пользователя
Что попадёт в записьпароль (по умолчанию)строка подключения целиком
Открытые соединенияпродолжают работатьпродолжают работать
Новые соединения со старым паролемотклоняются, пока сервис не перечитает значениепродолжают работать под активным пользователем

Разница только в новых подключениях. PostgreSQL проверяет пароль при установке соединения, поэтому уже открытые соединения ALTER USER не рвёт — ни в одной из стратегий. Промежуток, в котором новое подключение со старым паролем не пройдёт, есть только у single-user, и длится он от фазы set до момента, когда ваш сервис перечитает значение.

Что попадёт в запись: пароль или строка подключения

У dual-credential активная роль меняется каждый прогон, поэтому один пароль сервису бесполезен: под каким именем подключаться, он не узнает. Такая политика публикует строку подключения целиком:

postgres://app_b:pg_xxxxxxxx@db.internal:5432/billing

Имя и пароль приезжают вместе, одной версией одной записи — то есть атомарно. Хост, порт, база и параметры берутся из админского DSN: ротируемая роль ходит туда же, куда админ. Записи с такой политикой стоит и называть соответственно — DATABASE_URL, а не DB_PASSWORD.

У single-user имя не меняется никогда, поэтому по умолчанию публикуется только пароль. Если сервису удобнее готовый URL, форма даёт выбрать строку подключения и там — в конфиге это поле valueFormat:

valueFormatЧто в записи
не задано, dual-credential с двумя ролямистрока подключения
не задано, остальные случаипароль
"password"пароль
"dsn"строка подключения

Фаза test подключается ровно тем значением, которое получит сервис: собирать его заново значило бы проверить не то, что уедет потребителю.

Два одинаковых имени в usernames сервер не отклоняет при сохранении — прогон упадёт на фазе set с сообщением cannot determine the inactive username.

Что происходит с сервисами во время прогона

Пока прогон не дошёл до finish, сервисы получают прежнее значение: машине выдаётся версия под меткой current, а её переносит только finish. Незавершённый прогон в ленте — норма, а не поломка.

После finish новое значение подхватывается не мгновенно:

  • SDK держит записи в кэше 15 минут (cacheTtlMs), поэтому обычная задержка — до пятнадцати минут;
  • refresh() сбрасывает кэш и перечитывает немедленно — вызов по вебхуку entry.rotated превращает ротацию в событие для сервиса, а не в ожидание;
  • процесс, прочитавший значение один раз в переменную при старте, не узнает о смене никогда. Это довод не за ротацию, а за то, чтобы читать секрет через SDK.

Подробности — в документации SDK.

Метка pending

Между create и finish новое значение уже сохранено под меткой pending. Это не деталь реализации: если процесс упадёт после смены пароля в базе, но до записи в env-master, пароль будет потерян — а он уже действует. Значение становится durable раньше, чем начинает применяться.

Потребители в этот момент по-прежнему читают current и ничего не замечают.

Если прогон упал после create, новое значение так и остаётся под меткой pending — оно уже существует в целевой системе, и терять его нельзя. Следующий прогон создаёт своё значение и записывает его следующей версией; старое pending не применяется никогда. Такая версия остаётся в истории записи как обычная версия, созданная rotation-engine, но никогда не бывшая current.

Как читать прогон

У прогона есть статус и фаза — оба видны в ленте и в ответе GET /rotation-runs.

СтатусЧто значит
runningидёт фаза create, set или finish
testingидёт фаза test
succeededпрогон завершён, сервисы получают новое значение
failedпрерван на фазе phase; см. оговорку ниже

failed почти всегда означает «значение осталось прежним»: метка current переносится в самом конце, после успешного test. Единственное исключение — сбой внутри фазы finish уже после переноса метки: тогда прогон помечен failed, а новое значение сервисы уже получают. Поэтому «ротация не удалась» в ленте не читается как «секрет не менялся» — фаза в записи прогона говорит точнее статуса.

Статусы pending и rolled-back зарезервированы в типе, но движок их сейчас не выставляет.

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

Когда прогон падает

Наружу движок отдаёт только собственные сообщения. Ошибку драйвера СУБД он заменяет на «Rotation failed — see server logs for details»: в ней может оказаться строка подключения целиком. Подробности — в серверном логе, запись rotation run failed с policyId и runId.

СообщениеЧто случилось
admin DSN is not a valid connection stringзначение записи по adminDsnEntryRef не разбирается как адрес подключения. На фазе test нужен именно URL-формат
could not set the password for "X" on the target databaseне удалось выполнить ALTER USER: роли нет, нет прав у админской роли, база недоступна
the rotated credential for "X" was refused by the target databaseпароль сменён, но зайти под ним нельзя. Обычно — нет права CONNECT на базу из админского DSN или роль не LOGIN
cannot determine the inactive usernameв usernames два одинаковых имени
config.usernames must contain 1 or 2 usernamesпустой список или больше двух имён
invalid username in config.usernamesимя роли не подходит под [A-Za-z_][A-Za-z0-9_]*
Unknown rotator: Xротатора с таким именем нет; встроенный один — postgres
Pending value disappeared before finishзапись правили посреди прогона, и pending-версии не стало. Значение не опубликовано
Rotation run was interrupted and superseded by a newer runпрогон не падал: его процесс умер, а закрыл его следующий запуск
Secret reference is not accessible: …ответ на сохранение политики: ссылка ведёт в другую организацию или в проект, где у вас нет роли member

Неудачный прогон поднимает событие rotation.failed, вебхук и метрику. Значение при этом остаётся прежним — потребители не замечают сбоя, пока старое значение действует.

Политика

Полная форма запроса:

curl -s $BASE/entries/$ENTRY/rotation-policy \
  -X PUT -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
    "rotator": "postgres",
    "strategy": "dual-credential",
    "intervalSeconds": 604800,
    "config": {
      "adminDsnEntryRef": "my-org/billing-api/production/DB_ADMIN_DSN",
      "usernames": ["app_a", "app_b"]
    }
  }'
  • strategy — dual-credential или single-user;
  • intervalSeconds — не меньше 60;
  • adminDsnEntryRef — ссылка орг/проект/окружение/КЛЮЧ на запись с DSN администратора; ротатор читает её значение, сам DSN в конфиге не хранится;
  • enabled — необязательный, по умолчанию true;
  • valueFormat — необязательный: "password" или "dsn". Умолчание — "dsn" для dual-credential с двумя ролями и "password" во всех остальных случаях;
  • activeUsername в конфиг добавляет сам ротатор — руками его не задавайте.

PUT заменяет config целиком. При правке существующей политики сперва прочитайте её GET и перешлите activeUsername обратно — иначе ротатор начнёт отсчёт с первого имени в списке и сменит пароль пользователю, под которым сервисы сейчас работают.

Приостановить ротацию, не удаляя политику, — enabled: false. Снять совсем — DELETE /entries/:id/rotation-policy; значение и история версий остаются, меняться по расписанию оно перестаёт.

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

Ручной запуск

Внеочередной прогон — POST /rotation-policies/:id/run-now, история — GET /rotation-runs?policyId=....

Ручной запуск, пока идёт прогон той же политики, отклоняется с 409 CONFLICT — тик и run-now делят одну аренду политики. Приостановленную (enabled: false) политику run-now всё равно выполняет: это явная воля, а не расписание. Каждый ручной запуск пишется в журнал событием rotation.run.requested с именем запустившего.

Сводка по всей организации — GET /orgs/:id/rotations: последние прогоны (до 5 на политику, всего до 50) по проектам, где у вас есть роль; владелец и администратор организации видят все проекты. Та же лента — в кабинете, раздел «Ротации»: состояние каждого прогона, фаза, на которой он упал, и текст ошибки.

Управлять политиками из кабинета пока нельзя — только запросом PUT /entries/:id/rotation-policy, показанным выше.

Откат

Одно действие (POST /entries/:id/rollback, право — member): прежнее значение публикуется новой версией. История остаётся монотонной, повторный откат идёт вперёд, а не качает значение туда-обратно. Если предыдущей версии нет — 409 CONFLICT.

Откат меняет значение в env-master, но не в целевой системе: пароль в базе остаётся тем, который туда записала ротация. Откат ротируемой записи имеет смысл, только если вы вернёте пароль и в базе.

Какие ротаторы есть

Ротатор ровно один — postgres, обе стратегии (dual-credential и single-user). MySQL, Redis и Mongo встречаются в комментариях к коду как примеры точек расширения — встроенных ротаторов для них нет: политика с rotator: "mysql" отклоняется с ошибкой Unknown rotator. Каркас движка не завязан на конкретную СУБД: добавить ротатор значит реализовать четыре метода фаз.