Пароли БД утекают в логи: postgres_exporter печатает DSN при каждой неудачной сборке WAL-метрик (~210 строк в сутки) #3114

Closed
opened 2026-08-26 12:36:11 +00:00 by lekss361 · 2 comments
Owner

Найдено первым же запросом через новый Grafana-MCP, поэтому и в Loki уже лежит.

Что происходит

postgres_exporter при ошибке сборки печатает полный DSN, включая пароль:

level=ERROR source=postgres_exporter.go:681 msg="error scraping dsn"
err="queryNamespaceMappings returned 1 errors"
dsn="postgresql://gendesign_reader:<ПАРОЛЬ>@tradein-postgres:5432/tradein?sslmode=disable"

Ошибка повторяется при каждом скрейпе, потому что коллектор wal не имеет прав:

ERROR: permission denied for function pg_ls_waldir
collector failed name=wal err="pq: permission denied for function pg_ls_waldir"

Масштаб

Замер по Loki за 24 часа (count_over_time({container=~"gendesign-pg-exporter-.+"} |~ "postgresql://[^:]+:[^@]+@" [24h])):

экспортер строк с паролем за сутки
gendesign-pg-exporter-infra (Beget) 104
gendesign-pg-exporter-tradein (Poincare) 106

Итого около 210 строк в сутки. Ретенция Loki — 30 дней, значит в хранилище накапливается порядка 6000 строк с паролями БД.

Почему это важно именно сейчас

Логи централизованы в Loki, и доступ к ним даёт витрина Grafana. Сегодня же с неё снят внешний basic_auth (#3113, по решению владельца) — остался только вход Grafana. То есть глубина защиты уменьшилась ровно там, где появились пароли в логах. Сами по себе оба решения нормальны, но вместе дают неприятную комбинацию.

Как чинить

1. Убрать причину — выдать права. Канонический для postgres_exporter способ:

GRANT pg_monitor TO gendesign_reader;

pg_monitor — встроенная роль только для чтения статистики; прав на данные она не даёт. Ошибка исчезает, DSN перестаёт печататься. Нужно на обоих кластерах, где крутится экспортер.

2. Защита в глубину — не полагаться на отсутствие ошибок. Даже с правами любая другая ошибка скрейпа снова напечатает DSN. Варианты: отключить коллектор wal там, где он не нужен, либо передавать параметры подключения через DATA_SOURCE_URI + DATA_SOURCE_PASS_FILE вместо единой строки с паролем — тогда в лог попадать нечему.

3. Решить, что делать с уже накопленным. В Loki лежат пароли за прошедшие сутки и будут лежать 30 дней. Тщательный ответ — ротация паролей gendesign_reader на обоих кластерах после починки. Это решение владельца: ротация затрагивает боевые подключения.

Что готов сделать

Пункт 2 — код и PR. Пункты 1 и 3 трогают боевую БД и её учётные данные, поэтому за владельцем; скажешь — выполню грант.

Refs #3078

Найдено первым же запросом через новый Grafana-MCP, поэтому и в Loki уже лежит. ## Что происходит `postgres_exporter` при ошибке сборки печатает **полный DSN, включая пароль**: ``` level=ERROR source=postgres_exporter.go:681 msg="error scraping dsn" err="queryNamespaceMappings returned 1 errors" dsn="postgresql://gendesign_reader:<ПАРОЛЬ>@tradein-postgres:5432/tradein?sslmode=disable" ``` Ошибка повторяется при каждом скрейпе, потому что коллектор `wal` не имеет прав: ``` ERROR: permission denied for function pg_ls_waldir collector failed name=wal err="pq: permission denied for function pg_ls_waldir" ``` ## Масштаб Замер по Loki за 24 часа (`count_over_time({container=~"gendesign-pg-exporter-.+"} |~ "postgresql://[^:]+:[^@]+@" [24h])`): | экспортер | строк с паролем за сутки | |---|---| | `gendesign-pg-exporter-infra` (Beget) | **104** | | `gendesign-pg-exporter-tradein` (Poincare) | **106** | Итого около **210 строк в сутки**. Ретенция Loki — 30 дней, значит в хранилище накапливается порядка 6000 строк с паролями БД. ## Почему это важно именно сейчас Логи централизованы в Loki, и доступ к ним даёт витрина Grafana. Сегодня же с неё **снят внешний basic_auth** (#3113, по решению владельца) — остался только вход Grafana. То есть глубина защиты уменьшилась ровно там, где появились пароли в логах. Сами по себе оба решения нормальны, но вместе дают неприятную комбинацию. ## Как чинить **1. Убрать причину — выдать права.** Канонический для postgres_exporter способ: ```sql GRANT pg_monitor TO gendesign_reader; ``` `pg_monitor` — встроенная роль только для чтения статистики; прав на данные она не даёт. Ошибка исчезает, DSN перестаёт печататься. Нужно на обоих кластерах, где крутится экспортер. **2. Защита в глубину — не полагаться на отсутствие ошибок.** Даже с правами любая другая ошибка скрейпа снова напечатает DSN. Варианты: отключить коллектор `wal` там, где он не нужен, либо передавать параметры подключения через `DATA_SOURCE_URI` + `DATA_SOURCE_PASS_FILE` вместо единой строки с паролем — тогда в лог попадать нечему. **3. Решить, что делать с уже накопленным.** В Loki лежат пароли за прошедшие сутки и будут лежать 30 дней. Тщательный ответ — ротация паролей `gendesign_reader` на обоих кластерах после починки. Это решение владельца: ротация затрагивает боевые подключения. ## Что готов сделать Пункт 2 — код и PR. Пункты 1 и 3 трогают боевую БД и её учётные данные, поэтому за владельцем; скажешь — выполню грант. Refs #3078
Author
Owner

Причина устранена: гранты выданы, утечка прекратилась, WAL-метрика вернулась

По команде владельца выполнил пункт 1.

Что сделано

Экспортеров оказалось три, и роли у них разные — грант понадобился двум:

кластер роль было стало проверка тем вызовом, что падал
gendesign-infra-postgres (Beget) grafana_ro pg_monitor = false true pg_ls_waldir() → 27 сегментов
tradein-postgres (Poincare) gendesign_reader false true pg_ls_waldir() → 79 сегментов
postgres (Poincare, база gendesign) gendesign не трогал владелец базы, прав хватало

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

Проверка по результату, а не по факту выполнения команды

  • Ошибок в обоих экспортерах за 2 минуты после гранта — ноль (было ~4 в минуту).
  • WAL-метрика пошла со всех трёх баз: pg_wal_bytes_wal_segments = 63 (gendesign), 27 (infra), 79 (tradein). Цифры совпадают с прямыми запросами к БД — метрика не просто появилась, а показывает правду.
  • Утечка прекратилась: за последние 2 минуты в Loki ни одной строки с паролем. За 5 минут — 2 строки, обе до гранта.

Что осталось

Пункт 2PR #3115, скруббер в Alloy. Смержен. Он нужен и после устранения причины: ловит любой пароль в URL, включая компоненты, о которых мы ещё не знаем. Заодно там зафиксировано измерением, что напрашивавшийся способ (передавать пароль отдельным файлом) не работает — экспортер собирает строку сам.

Пункт 3 остаётся открытым и он за владельцем: ротировать ли grafana_ro и gendesign_reader. Пароли пролежали в Loki около суток и останутся там 30 дней по ретенции, а доступ к хранилищу даёт вход в Grafana — с которой сегодня снят внешний basic_auth (#3113). Если решишь ротировать — подготовлю смену с перезапуском экспортеров; сами по себе они read-only, так что риск невелик.

Альтернатива ротации — досрочно удалить затронутые потоки из Loki, но это сложнее и оставляет вопрос резервных копий хранилища.

## Причина устранена: гранты выданы, утечка прекратилась, WAL-метрика вернулась По команде владельца выполнил пункт 1. ### Что сделано Экспортеров оказалось три, и роли у них **разные** — грант понадобился двум: | кластер | роль | было | стало | проверка тем вызовом, что падал | |---|---|---|---|---| | `gendesign-infra-postgres` (Beget) | `grafana_ro` | `pg_monitor` = false | true | `pg_ls_waldir()` → 27 сегментов | | `tradein-postgres` (Poincare) | `gendesign_reader` | false | true | `pg_ls_waldir()` → 79 сегментов | | `postgres` (Poincare, база gendesign) | `gendesign` | — | не трогал | владелец базы, прав хватало | Третий экспортер ходит под владельцем базы и не падал — это сходится с исходным замером, где утечка была ровно у двух контейнеров. ### Проверка по результату, а не по факту выполнения команды - **Ошибок в обоих экспортерах за 2 минуты после гранта — ноль** (было ~4 в минуту). - **WAL-метрика пошла со всех трёх баз:** `pg_wal_bytes_wal_segments` = 63 (gendesign), 27 (infra), 79 (tradein). Цифры совпадают с прямыми запросами к БД — метрика не просто появилась, а показывает правду. - **Утечка прекратилась:** за последние 2 минуты в Loki **ни одной** строки с паролем. За 5 минут — 2 строки, обе до гранта. ### Что осталось **Пункт 2** — [PR #3115](https://git.gendsgn.ru/lekss361/gendesign/pulls/3115), скруббер в Alloy. Смержен. Он нужен и после устранения причины: ловит любой пароль в URL, включая компоненты, о которых мы ещё не знаем. Заодно там зафиксировано измерением, что напрашивавшийся способ (передавать пароль отдельным файлом) **не работает** — экспортер собирает строку сам. **Пункт 3 остаётся открытым и он за владельцем:** ротировать ли `grafana_ro` и `gendesign_reader`. Пароли пролежали в Loki около суток и останутся там 30 дней по ретенции, а доступ к хранилищу даёт вход в Grafana — с которой сегодня снят внешний basic_auth (#3113). Если решишь ротировать — подготовлю смену с перезапуском экспортеров; сами по себе они read-only, так что риск невелик. Альтернатива ротации — досрочно удалить затронутые потоки из Loki, но это сложнее и оставляет вопрос резервных копий хранилища.
Collaborator

Решение владельца: пароли НЕ ротируем

Спросил статусом, ответ прямой — «пароли не ротируем». Фиксирую как принятое решение, чтобы к нему не возвращались как к висящему вопросу.

Что это значит по пунктам:

  • П.1 (причина) — закрыт. Гранты pg_monitor выданы, ошибки в обоих экспортерах прекратились, WAL-метрика пошла со всех трёх баз с числами, совпадающими с прямыми запросами.
  • П.2 (защита в глубину) — закрыт. Скруббер в Alloy смержен (#3115).
  • П.3 (что делать с накопленным) — решение: ничего. Пароли grafana_ro и gendesign_reader остаются прежними. Строки в Loki доживут до конца 30-суточной ретенции и уйдут сами; досрочное удаление потоков не делаем.

Что принимается вместе с этим решением, чтобы это было записано явно: обе роли — только на чтение статистики (pg_monitor + reader-гранты), доступ к их паролям даёт наблюдение, но не запись; строки лежат в хранилище, вход в которое — только через Grafana. Риск сочтён приемлемым.

Закрываю issue.


Отдельно, чтобы не потерялось: сегодня нашёлся второй канал того же класса, к этим ролям отношения не имеющий — секрет вебхука GlitchTip уходит в Loki query-параметром, и скруббер #3115 его не ловит (он написан под форму ://user:pass@). Вынес в #3154.

## Решение владельца: пароли НЕ ротируем Спросил статусом, ответ прямой — «пароли не ротируем». Фиксирую как принятое решение, чтобы к нему не возвращались как к висящему вопросу. **Что это значит по пунктам:** - **П.1 (причина) — закрыт.** Гранты `pg_monitor` выданы, ошибки в обоих экспортерах прекратились, WAL-метрика пошла со всех трёх баз с числами, совпадающими с прямыми запросами. - **П.2 (защита в глубину) — закрыт.** Скруббер в Alloy смержен (#3115). - **П.3 (что делать с накопленным) — решение: ничего.** Пароли `grafana_ro` и `gendesign_reader` остаются прежними. Строки в Loki доживут до конца 30-суточной ретенции и уйдут сами; досрочное удаление потоков не делаем. **Что принимается вместе с этим решением, чтобы это было записано явно:** обе роли — только на чтение статистики (`pg_monitor` + reader-гранты), доступ к их паролям даёт наблюдение, но не запись; строки лежат в хранилище, вход в которое — только через Grafana. Риск сочтён приемлемым. Закрываю issue. --- Отдельно, чтобы не потерялось: сегодня нашёлся **второй канал того же класса**, к этим ролям отношения не имеющий — секрет вебхука GlitchTip уходит в Loki query-параметром, и скруббер #3115 его не ловит (он написан под форму `://user:pass@`). Вынес в #3154.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3114
No description provided.