alert-ack: значение секрета вебхука GlitchTip больше не пишется в лог — строка доступа и ошибки разбора дают secret=*** #3588
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3588
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3576-alert-ack-redact-secret"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что изменено
ops/metrics/alert-ack/app.py:Handler.log_messageмаскирует значения секретных query-параметров (secret,token,*_secret,*_token,api_key,password,sig… — то же выражение, что у бэкенда МЕРЫ вtradein-mvp/backend/app/core/log_scrub.pyиз #3154 и у Alloy из #3354; совпадение проверено побайтно). Импортировать выражение нельзя: сервис намеренно без зависимостей, в контейнер монтируется одинapp.py.Почему именно
log_message: вBaseHTTPRequestHandlerстрока запроса попадает в лог двумя путями, и оба проходят через этот метод:log_request→'"%s" %s %s' % (requestline, code, size);log_error→code 400, message Bad request syntax ('POST /glitchtip?secret=… junk HTTP/1.1'), а следом та же строка ещё раз в строке доступа с кодом 400.Свои записи сервиса (
glitchtip webhook: invalid or missing secretи т.п.) ни пути, ни значения не содержат.Улики (значение секрета нигде не приводится)
Код:
log.info("%s %s", self.address_string(), fmt % args), строка запроса выводится целиком. Формат со значением вdocker logsзафиксирован в самой задаче (09:35 17.09).Beget, только чтение: контейнер пересоздан деплоем в 2026-09-17T12:50Z, в его
docker logsстрок сsecret=пока 0 (GlitchTip с тех пор не присылал). Лог-драйверjournald, но журнал прежних контейнеров пользователю деплоя недоступен (не в группеadm), поэтому там не проверял.Loki. Утверждение задачи «секрет лежит и в Loki» не подтвердилось. Запросы
query_rangeза 29 суток, выполнены внутриgendesign-loki:{container=~".*alert-ack.*"} |~ "(?i)secret=": 106 строк, из них 106 сsecret=***, значения нет ни в одной. Период 12.09–17.09, то есть с появления/glitchtip;{host="infra"} |~ "(?i)[?&][a-z_.-]*secret=[^*]": 0 строк по всему инфра-хосту, включая Caddy.Значение маскирует стадия
stage.replaceвalloy-infra.alloy(#3354) до отправки. Открытым текстом значение лежит только локально на Beget: вdocker logsи в journald. Эта правка закрывает первый слой, в самом процессе, как и #3154 для бэкенда МЕРЫ. По пункту 3 задачи: за срок хранения Loki чистить нечего.Тесты
Тест добавлен в
backend/tests/ops/test_3078_alert_ack.py:test_sekret_iz_query_ne_popadaet_v_log, три параметра. Сервер поднимается на настоящем сокете, записи лога ловитcaplog. Проверки по значению: выдуманного значения нет ни в одной записи, а…=***стоит в ожидаемом числе записей (1 / 1 / 2). Второе условие не даёт тесту позеленеть, если лог просто замолчит.POST /glitchtip?secret=<проба>: строка доступа;GET /ack/x?a=1&access_token=<проба>&b=2: имя с префиксом, соседние параметры сохраняются;POST /glitchtip?secret=<проба> junk HTTP/1.1: ошибка разбора, две записи.Локально (Python 3.12, venv бэкенда):
pytest tests/ops/test_3078_alert_ack.py: 12 passed, rc=0;pytest tests/ops/: 145 passed, 4 failed, rc=1. Все 4 падения вtest_2203_backup_trailer_grep_dashdash.py(mktemp: unrecognized option --suffix— на macOS нет GNU mktemp). Эти тесты не касаются изменённых файлов. Гейт доездаtest_metrics_single_file_mounts.pyиз #3557 зелёный;ops/metrics/alert-ack/test_app.py: 13 passed, rc=0;ruff checkиruff formatтестового файла: чисто. Pre-commit (ruff, ruff format, detect private key) пройден.CI. Правка
ops/**включаетbackend=trueвci.yml, иbackend-testsгоняетbackend/tests/ops/, так что новый тест в CI исполняется. При этомops/metrics/alert-ack/test_app.pyне запускает ни одна джоба: у бэкендаtestpaths = ["tests"], аci-tradein.ymlэти пути не собирает. Там 13 тестов, в том числе keep-alive-тесты с прода от 12.09. Дыра существовала до этого PR и здесь не чинится, поэтому новый тест положен в CI-исполняемый файл.Фальсификация (без stash: копия файла → порча → возврат копии →
cmp)log_messageснята только маскировка (fmt % argsбезsub): 3 failed, rc=1. Каждый параметр красный сAssertionError: значение секрета в логе, в том числе на записи ошибки разбораcode 400, message Bad request syntax (...).log_messageзаменено наpass, лог молчит: 3 failed, rc=1 (срабатывает проверка числа записей с=***).После возврата
cmpпоказал совпадение с исправленной версией, прогон: 12 passed, rc=0.Как доезжает
deploy-metrics.ymlзапускается поops/metrics/**. В джобеserverпри профилеalertsбезусловно выполняетсяup -d --force-recreate alert-ack(#3490). Гейтtest_metrics_single_file_mounts.pyиз #3557 считает это путём доезда для пофайлового маунта./ops/metrics/alert-ack/app.py:/app/app.py:roи сейчас зелёный. На проде маунт/opt/gendesign/ops/metrics/alert-ack/app.py → /app/app.py.Приёмка после деплоя (Beget)
Код в контейнере, а не на диске. Маркер, которого нет в старой версии:
docker exec gendesign-alert-ack grep -c _SENSITIVE_QUERY /app/app.py→ ≥ 1. Инод:stat -c %i /opt/gendesign/ops/metrics/alert-ack/app.pyравенdocker exec gendesign-alert-ack stat -c %i /app/app.py.Тестовый запрос с выдуманным значением, не с настоящим секретом:
curl -s -o /dev/null -w '%{http_code}\n' -X POST 'https://metrics.gendsgn.ru/glitchtip?secret=probe3576x' -H 'Content-Type: application/json' -d '{}'→401. Затем:docker logs --since 10m gendesign-alert-ack 2>&1 | grep -c probe3576x→ 0;docker logs --since 10m gendesign-alert-ack 2>&1 | grep -c 'glitchtip?secret=\*\*\*'→ ≥ 1.Проверка различает версии: на старом коде первая команда дала бы ≥ 1.
При следующем настоящем алерте GlitchTip в
docker logsпоявится"POST /glitchtip?secret=*** HTTP/1.1" 200 -.Ротация секрета — за владельцем
В этом PR не делается. Значение уже осело в
docker logs/journald прежних контейнеров на Beget, а PR историю не чистит. Менять нужно одновременноALERT_ACK_GLITCHTIP_SECRETв env alert-ack и URL получателя GlitchTip (AlertRecipientid=6). Пункт приёмки задачи «старый секрет → 401, новый принимается» проверяется только после ротации, поэтому здесьRefs, а неCloses.Вне объёма
/ack/<token>стоит в пути, а не в query. Он пишется в лог как есть и не маскируется ни здесь, ни в Alloy, то есть лежит в Loki. Ссылка одноразовая и живёт 24 часа, но с доступом к Grafana можно нажать «принято» за дежурного. Не трогал; если это важно, нужна отдельная задача.secr%65t=) выражение не ловит, хотяparse_qsего декодирует. Отправить такое может только тот, кто уже знает секрет, поэтому оставлено.Refs #3576
🤖 Generated with Claude Code
GlitchTip шлёт секрет резервного вебхука только в `?secret=`, а BaseHTTPRequestHandler печатает строку запроса целиком: в строке доступа (log_request) и в тексте ошибки разбора (log_error, «Bad request syntax ('POST /glitchtip?secret=…')»). Оба пути сходятся в log_message — маскируем там одним выражением, тем же, что у бэкенда МЕРЫ (#3154, log_scrub.py) и у Alloy (#3354). Импортировать его нельзя: сервис намеренно без зависимостей. Тест в backend/tests/ops/test_3078_alert_ack.py (его гоняет CI по ops/**): настоящий сокет, три строки запроса — доступ, имя с префиксом, ошибка разбора; значения в записях нет, `=***` стоит в ожидаемом числе записей. Ротация секрета — за владельцем, здесь не делается. Refs #3576 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Состояние до деплоя (17.09, Beget, только чтение), чтобы было с чем сравнивать приёмку:
docker exec gendesign-alert-ack grep -c _SENSITIVE_QUERY /app/app.py→0. Маркера в работающем коде нет, значит пункт 1 приёмки различает версии;585871=585871;grepиstatв образеpython:3.12-slimесть, команды приёмки исполнимы как написаны.