Секрет вебхука GlitchTip уходит в Loki: uvicorn логирует query-параметр, скруббер #3115 его не ловит #3154

Open
opened 2026-08-27 16:54:58 +00:00 by bot-backend · 1 comment
Collaborator

Найдено при сквозной проверке #2673 — вебхук заработал сегодня, и вместе с ним появилась эта строка в логах.

Что происходит

Секрет вебхука едет query-параметром, и это не оплошность: GlitchTip шлёт вебхуки через send_webhook() без единого заголовка (ни Authorization, ни подписи — см. докстринг tradein-mvp/backend/app/api/v1/glitchtip.py), поэтому аутентифицировать вызов больше нечем. Но uvicorn пишет в access-log полный URL:

INFO:  172.18.0.3:60322 - "POST /api/v1/trade-in/ops/glitchtip-webhook?secret=<64 hex> HTTP/1.1" 200 OK

А лог уезжает в Loki. Проверено запросом к Loki ({container="tradein-backend"} |= "secret=") — строки лежат там незамаскированными.

Почему скруббер #3115 не помогает

ops/metrics/alloy/alloy-apps.alloy:171 маскирует ровно одно выражение:

expression = "://[^:@/ ]+:([^@ ]+)@"

Это форма scheme://user:pass@host. Query-параметр ?secret=… под неё не подходит — тот скруббер писался под DSN postgres_exporter (#3114) и честно делает только это.

Масштаб

До сегодня строк не было вовсе: вебхук отвечал 503, до логирования URL дело не доходило. Сейчас их 3 — все от проверочных проб. Дальше будет по строке на каждый доставленный алерт: три правила с окном 5 минут и порогом 1, плюс uptime.

Ретенция Loki — 30 суток, доступ к хранилищу даёт вход в Grafana (внешний basic_auth с неё снят, #3113). То есть засветка не теоретическая.

Отдельно: TRADEIN_INTERNAL_AUTH_SECRET — не только ключ вебхука. Тот же секрет используется как второй рубеж в app.core.rbac (#2213), так что его утечка шире по последствиям, чем «кто-то пришлёт нам фальшивый алерт».

Варианты починки

1. Не логировать (дёшево, локально). Фильтр на uvicorn.access, вырезающий значение query-параметров из списка чувствительных (secret, token, api_key) до записи строки. Чинит причину в нашем процессе, не трогает инфраструктуру.

2. Скруббер в Alloy (защита в глубину). Второй stage.replace на ([?&](?:secret|token|api_key)=)[^&\s"]+. Ловит и то, о чём мы ещё не знаем, — ровно тем же доводом, каким обосновывался #3115. Не заменяет п.1: в логе контейнера строка всё равно остаётся.

3. Убрать секрет из URL совсем (структурно). Caddy умеет то, чего не умеет GlitchTip: на маршруте вебхука превратить query-параметр в заголовок (header_up + снять query) и проксировать дальше уже чистый путь. Тогда бэкенд логирует URL без секрета, а проверка переезжает на заголовок. Дороже двух первых и трогает боевой Caddy.

Рекомендую 1 + 2 сразу, 3 — отдельным решением.

Про ротацию

Секрет пролежал в Loki около часа в трёх строках. Ротация сейчас дешевле, чем потом, но это решение владельца: TRADEIN_INTERNAL_AUTH_SECRET придётся менять одновременно в рантайм-конфиге и в трёх URL получателей GlitchTip, иначе алерты начнут получать 401.

Refs #2673, #3114, #3115

Найдено при сквозной проверке #2673 — вебхук заработал сегодня, и вместе с ним появилась эта строка в логах. ## Что происходит Секрет вебхука едет **query-параметром**, и это не оплошность: GlitchTip шлёт вебхуки через `send_webhook()` без единого заголовка (ни `Authorization`, ни подписи — см. докстринг `tradein-mvp/backend/app/api/v1/glitchtip.py`), поэтому аутентифицировать вызов больше нечем. Но uvicorn пишет в access-log полный URL: ``` INFO: 172.18.0.3:60322 - "POST /api/v1/trade-in/ops/glitchtip-webhook?secret=<64 hex> HTTP/1.1" 200 OK ``` А лог уезжает в Loki. Проверено запросом к Loki (`{container="tradein-backend"} |= "secret="`) — строки лежат там **незамаскированными**. ## Почему скруббер #3115 не помогает `ops/metrics/alloy/alloy-apps.alloy:171` маскирует ровно одно выражение: ``` expression = "://[^:@/ ]+:([^@ ]+)@" ``` Это форма `scheme://user:pass@host`. Query-параметр `?secret=…` под неё не подходит — тот скруббер писался под DSN postgres_exporter (#3114) и честно делает только это. ## Масштаб До сегодня строк не было вовсе: вебхук отвечал 503, до логирования URL дело не доходило. Сейчас их **3** — все от проверочных проб. Дальше будет по строке на каждый доставленный алерт: три правила с окном 5 минут и порогом 1, плюс uptime. Ретенция Loki — 30 суток, доступ к хранилищу даёт вход в Grafana (внешний basic_auth с неё снят, #3113). То есть засветка не теоретическая. Отдельно: `TRADEIN_INTERNAL_AUTH_SECRET` — не только ключ вебхука. Тот же секрет используется как второй рубеж в `app.core.rbac` (#2213), так что его утечка шире по последствиям, чем «кто-то пришлёт нам фальшивый алерт». ## Варианты починки **1. Не логировать (дёшево, локально).** Фильтр на `uvicorn.access`, вырезающий значение query-параметров из списка чувствительных (`secret`, `token`, `api_key`) до записи строки. Чинит причину в нашем процессе, не трогает инфраструктуру. **2. Скруббер в Alloy (защита в глубину).** Второй `stage.replace` на `([?&](?:secret|token|api_key)=)[^&\s"]+`. Ловит и то, о чём мы ещё не знаем, — ровно тем же доводом, каким обосновывался #3115. Не заменяет п.1: в логе контейнера строка всё равно остаётся. **3. Убрать секрет из URL совсем (структурно).** Caddy умеет то, чего не умеет GlitchTip: на маршруте вебхука превратить query-параметр в заголовок (`header_up` + снять query) и проксировать дальше уже чистый путь. Тогда бэкенд логирует URL без секрета, а проверка переезжает на заголовок. Дороже двух первых и трогает боевой Caddy. Рекомендую 1 + 2 сразу, 3 — отдельным решением. ## Про ротацию Секрет пролежал в Loki около часа в трёх строках. Ротация сейчас дешевле, чем потом, но это решение владельца: `TRADEIN_INTERNAL_AUTH_SECRET` придётся менять одновременно в рантайм-конфиге и в трёх URL получателей GlitchTip, иначе алерты начнут получать 401. Refs #2673, #3114, #3115
bot-backend added the
bug
observability
priority/p2
scope/devops
security
tradein
labels 2026-08-27 16:54:59 +00:00
Author
Collaborator

Прод-приёмка PR #3353 (05.09, после деплоя e48b4a4): запрос GET /trade-in/api/v1/me?secret=abc123SENTINEL&x=1 → в живом access-log tradein-backend строка "GET /api/v1/me?secret=***&x=1 HTTP/1.1" 401; вхождений сентинела в логах за окно — 0, замаскированных — 1. Caddy access-логи (/var/log/caddy/*) вхождений secret= не содержат — второй канал утечки отсутствует.

Что остаётся до закрытия issue (не код):

  1. Ротация TRADEIN_INTERNAL_AUTH_SECRET — прежние строки в Loki (ретенция 30 суток) фикс не отзывает; менять в рантайм-конфиге прода и в URL получателей GlitchTip одновременно — решение/действие владельца.
  2. #3354 — второй слой скруббинга в Alloy (defence-in-depth).
  3. Опционально: переезд вебхука на заголовок X-GlitchTip-Secret через Caddy header_up + снятие query на маршруте (GlitchTip 6.1.6 сам заголовок не шлёт).
Прод-приёмка PR #3353 (05.09, после деплоя e48b4a4): запрос `GET /trade-in/api/v1/me?secret=abc123SENTINEL&x=1` → в живом access-log tradein-backend строка `"GET /api/v1/me?secret=***&x=1 HTTP/1.1" 401`; вхождений сентинела в логах за окно — **0**, замаскированных — 1. Caddy access-логи (`/var/log/caddy/*`) вхождений `secret=` не содержат — второй канал утечки отсутствует. Что остаётся до закрытия issue (не код): 1. **Ротация `TRADEIN_INTERNAL_AUTH_SECRET`** — прежние строки в Loki (ретенция 30 суток) фикс не отзывает; менять в рантайм-конфиге прода и в URL получателей GlitchTip одновременно — решение/действие владельца. 2. #3354 — второй слой скруббинга в Alloy (defence-in-depth). 3. Опционально: переезд вебхука на заголовок `X-GlitchTip-Secret` через Caddy `header_up` + снятие query на маршруте (GlitchTip 6.1.6 сам заголовок не шлёт).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3154
No description provided.