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
Версия 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
67 lines
3.6 KiB
Python
67 lines
3.6 KiB
Python
"""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))}"
|
||
)
|