fix(ops/metrics): postgres-exporter печатал пароль БД в лог при каждой ошибке #3144

Merged
lekss361 merged 1 commit from fix/3114-pg-exporter-leaks-dsn into main 2026-08-27 13:15:01 +00:00
2 changed files with 89 additions and 3 deletions

View file

@ -0,0 +1,67 @@
"""postgres-exporter не должен печатать пароль БД в лог.
ЧТО БЫЛО. Версия v0.16.0 при КАЖДОЙ ошибке сбора печатала полный DSN вместе с
паролем:
msg="error scraping dsn" err="queryNamespaceMappings returned 1 errors"
dsn="postgresql://<роль>:<ПАРОЛЬ>@<хост>:5432/<база>?sslmode=disable"
Строки уходили в Loki около 210 в сутки с двух хостов, при ретенции 30 дней
это тысячи паролей в хранилище логов (#3114).
ЧТО ПРОВЕРЕНО ОПЫТОМ 27.08, а не взято из документации. Стенд: чистый постгрес
в скретч-контейнере, роль без прав, тот же `ops/metrics/postgres/queries.yml`,
что и на проде то есть ровно та ошибка, что случалась в бою.
v0.16.0 строк с паролем: 1
v0.18.0 строк с паролем: 0
Отдельно опровергнут обходной путь, который предлагался в issue: форма
`DATA_SOURCE_URI` + `DATA_SOURCE_USER` + `DATA_SOURCE_PASS` НЕ помогает
экспортер собирает DSN внутри и печатает его целиком точно так же (тот же
стенд, тот же результат: 1 строка с паролем). То есть починка «через
переменные окружения» была бы работой вхолостую.
Паритет метрик проверен там же: все пять пользовательских запросов отдаются
обеими версиями одинаково, v0.18 добавляет три встроенные и не теряет ни одной.
Тест сторожит только нижнюю границу версии: откат на v0.16.x вернул бы утечку.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
COMPOSE = REPO_ROOT / "docker-compose.metrics-agent.yml"
# Первая версия, в которой пароль в лог не попадает (проверено опытом, см. шапку).
MIN_SAFE = (0, 18, 0)
_TAG = re.compile(r"postgres-exporter:v(\d+)\.(\d+)\.(\d+)")
def _versions() -> list[tuple[int, int, int]]:
text = COMPOSE.read_text(encoding="utf-8")
return [(int(a), int(b), int(c)) for a, b, c in _TAG.findall(text)]
def test_all_three_exporters_are_pinned() -> None:
"""Экспортеров три — на всех трёх версия должна быть явной.
Страховка от того, что тест ниже начнёт проверять пустоту: если кто-то
заменит тег на `latest` или удалит сервис, здесь станет видно.
"""
assert len(_versions()) == 3, (
f"ожидались три закреплённые версии postgres-exporter, найдено: {_versions()}"
)
def test_no_exporter_older_than_the_leaking_one() -> None:
"""Ни один экспортер не смеет быть старше версии, где утечка закрыта."""
for v in _versions():
assert v >= MIN_SAFE, (
f"postgres-exporter v{'.'.join(map(str, v))} печатает пароль БД в лог "
f"при каждой ошибке сбора — минимум v{'.'.join(map(str, MIN_SAFE))}"
)

View file

@ -163,8 +163,27 @@ services:
# ── postgres-exporter: Птица (только на Poincare) ────────────────────────────
# WAL/сутки, n_tup_upd против n_tup_hot_upd, рост TOAST, коннекты, горизонт
# vacuum — то, из-за отсутствия чего раздутие копилось 91 день незамеченным.
# v0.18.0, а не v0.16.0: та печатала ПОЛНЫЙ DSN вместе с паролем в каждую
# ошибку сбора (#3114). Строка вида
# msg="error scraping dsn" err="queryNamespaceMappings returned 1 errors"
# dsn="postgresql://<роль>:<ПАРОЛЬ>@<хост>:5432/<база>?sslmode=disable"
# уходила в Loki при каждом скрейпе — около 210 строк в сутки с двух хостов,
# при ретенции 30 дней это тысячи паролей в хранилище логов.
#
# Воспроизведено на скретч-контейнерах 27.08 (постгрес + роль без прав +
# этот же queries.yml): v0.16.0 печатает пароль, v0.18.0 — нет.
#
# NB: форма DATA_SOURCE_URI + DATA_SOURCE_USER + DATA_SOURCE_PASS, которую
# предлагали как обходной путь, НЕ помогает — проверено тем же опытом:
# экспортер собирает DSN внутри и печатает его целиком точно так же.
# Утечку закрывает только версия образа.
#
# Паритет метрик проверен на том же стенде: все пять пользовательских
# запросов из queries.yml отдаются обеими версиями одинаково
# (PG_EXPORTER_EXTEND_QUERY_PATH в v0.18 работает), v0.18 добавляет три
# встроенные метрики и не теряет ни одной.
postgres-exporter-gendesign:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-gendesign
restart: unless-stopped
profiles: ["apps"]
@ -192,7 +211,7 @@ services:
# ── postgres-exporter: МЕРА (только на Poincare) ─────────────────────────────
postgres-exporter-tradein:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-tradein
restart: unless-stopped
profiles: ["apps"]
@ -216,7 +235,7 @@ services:
# forgejo + glitchtip. Нужен и сам по себе, и как страховка: рост базы glitchtip
# ничем не ограничен — политики ретенции у GlitchTip нет вообще.
postgres-exporter-infra:
image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-infra
restart: unless-stopped
profiles: ["infra"]