diff --git a/backend/tests/ops/test_3114_pg_exporter_version.py b/backend/tests/ops/test_3114_pg_exporter_version.py new file mode 100644 index 00000000..6a3e2092 --- /dev/null +++ b/backend/tests/ops/test_3114_pg_exporter_version.py @@ -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))}" + ) diff --git a/docker-compose.metrics-agent.yml b/docker-compose.metrics-agent.yml index e351a56f..506be509 100644 --- a/docker-compose.metrics-agent.yml +++ b/docker-compose.metrics-agent.yml @@ -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"]