gendesign/backend/tests/ops/test_3114_pg_exporter_version.py
bot-backend b746134976
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Successful in 17m34s
fix(ops/metrics): postgres-exporter печатал пароль БД в лог при каждой ошибке
Версия v0.16.0 при КАЖДОЙ неудаче сбора печатала полный DSN вместе с паролем:

    msg="error scraping dsn" err="queryNamespaceMappings returned 1 errors"
    dsn="postgresql://<роль>:<ПАРОЛЬ>@<хост>:5432/<база>?sslmode=disable"

Строки уходят в Loki — около 210 в сутки с двух хостов, при ретенции 30 дней
это тысячи паролей в хранилище логов (#3114).

Проверял опытом, а не документацией. Стенд на скретч-контейнерах: чистый
постгрес, роль без прав, тот же queries.yml что на проде — то есть ровно та
ошибка, что случалась в бою.

    v0.16.0 → строк с паролем: 1
    v0.18.0 → строк с паролем: 0

ОБХОДНОЙ ПУТЬ ИЗ ISSUE НЕ РАБОТАЕТ, и это важнее самой правки. Предлагалось
передавать параметры через DATA_SOURCE_URI + DATA_SOURCE_USER + DATA_SOURCE_PASS
вместо единой строки — «тогда в лог попадать нечему». Проверил на том же
стенде: экспортер собирает DSN внутри и печатает его целиком точно так же,
1 строка с паролем. Реализация этого варианта была бы работой вхолостую при
полном ощущении, что дыра закрыта.

Паритет метрик проверен там же: все пять пользовательских запросов из
queries.yml отдаются обеими версиями одинаково (PG_EXPORTER_EXTEND_QUERY_PATH
в v0.18 работает), v0.18 добавляет три встроенные метрики и не теряет ни одной.

Два теста сторожат нижнюю границу версии на всех трёх экспортерах.
Прогон: 80 ops-тестов зелёные, ruff чист.

Refs #3114
2026-08-27 15:55:24 +03:00

67 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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))}"
)