[P1] Defense-in-depth для trusted-header auth + rate-limiter fix #2213

Closed
opened 2026-07-02 19:18:46 +00:00 by lekss361 · 4 comments
Owner

Эпик: #2200

Находка (Security, High, verified live): изнутри docker-сети curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/...200 полный админ; без заголовка → 401. main.py:125 доверяет заголовку без верификации (нет shared-secret/mTLS/source-IP). Backend на общей gendesign_shared сети с ОБОИМИ стеками → blast radius любого SSRF/скомпрометированного контейнера тотальный. Плюс rate-limiter (ratelimit.py:42-43) освобождает КАЖДЫЙ авторизованный запрос и ключит на клиент-контролируемый XFF — плацебо. log_credentials глобально пишет обратимые basic-auth пароли на диск (Caddyfile:25).

Acceptance:

  • Defense-in-depth: shared-secret/mTLS между Caddy и backend ИЛИ network-policy изоляция порта 8000 от соседей (не только Caddy header_up).
  • Rate-limiter: реальный per-user throttle на /search и /geocode; не доверять leftmost XFF.
  • Ревью log_credentials (нужен ли; retention/доступ к логам).
  • Тест/проба, что подделка заголовка изнутри сети больше не даёт admin.

Scope: tradein-mvp/backend/app/main.py, core/ratelimit.py, Caddyfile, compose network.

Эпик: #2200 **Находка (Security, High, verified live):** изнутри docker-сети `curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/...` → **200 полный админ**; без заголовка → 401. `main.py:125` доверяет заголовку без верификации (нет shared-secret/mTLS/source-IP). Backend на общей `gendesign_shared` сети с ОБОИМИ стеками → blast radius любого SSRF/скомпрометированного контейнера тотальный. Плюс rate-limiter (`ratelimit.py:42-43`) освобождает КАЖДЫЙ авторизованный запрос и ключит на клиент-контролируемый XFF — плацебо. `log_credentials` глобально пишет обратимые basic-auth пароли на диск (Caddyfile:25). **Acceptance:** - [ ] Defense-in-depth: shared-secret/mTLS между Caddy и backend ИЛИ network-policy изоляция порта 8000 от соседей (не только Caddy header_up). - [ ] Rate-limiter: реальный per-user throttle на /search и /geocode; не доверять leftmost XFF. - [ ] Ревью `log_credentials` (нужен ли; retention/доступ к логам). - [ ] Тест/проба, что подделка заголовка изнутри сети больше не даёт admin. Scope: `tradein-mvp/backend/app/main.py`, `core/ratelimit.py`, `Caddyfile`, compose network.
lekss361 added the
priority/p1
scope/backend
scope/devops
security
tradein
labels 2026-07-02 19:20:24 +00:00
Author
Owner

Working on this (night shift), ветка fix/tradein-defense-in-depth-auth. Дизайн: shared-secret между Caddy и tradein-backend (X-Internal-Auth-Secret, constant-time compare, fail-open с WARNING до провижининга — значение секрета только в VPS .env, не в repo) + rate-limiter per-user без leftmost-XFF + разбор log_credentials. Сеть не перекраиваю (рискованно ночью).

Working on this (night shift), ветка `fix/tradein-defense-in-depth-auth`. Дизайн: shared-secret между Caddy и tradein-backend (`X-Internal-Auth-Secret`, constant-time compare, fail-open с WARNING до провижининга — значение секрета только в VPS .env, не в repo) + rate-limiter per-user без leftmost-XFF + разбор log_credentials. Сеть не перекраиваю (рискованно ночью).
Author
Owner

PR #2229 смержен (код-часть). Статус acceptance:

  • Defense-in-depth код: shared-secret Caddy↔backend (constant-time), fail-open до провижининга; adversarial-ревью прошёл (SSR-форжинг невозможен, bypass-анализ чист).
  • Rate-limiter: реальный per-user/per-IP throttle, leftmost XFF нейтрализован (тест).
  • log_credentials убран из Caddyfile (glitchtip-forwarder деградирует gracefully).
  • needs-human — провижининг секрета (без него защита fail-open = не активна):
    1. openssl rand -hex 32
    2. СНАЧАЛА Caddy-стек: значение TRADEIN_INTERNAL_AUTH_SECRET в env-файл /opt/gendesign/recreate caddy (reload не перечитает env).
    3. ПОТОМ tradein-стек: то же значение в рантайм-env tradein-backend → restart backend.
    4. Обратный порядок = fail-closed 401 на всё приложение. Значения обязаны совпадать.
    5. Проверка после: изнутри сети curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/... → 401.

Issue оставляю открытым до провижининга (последний acceptance-пункт «подделка изнутри сети не даёт admin» проверяется только после включения секрета).

PR #2229 смержен (код-часть). Статус acceptance: - ✅ Defense-in-depth код: shared-secret Caddy↔backend (constant-time), fail-open до провижининга; adversarial-ревью прошёл (SSR-форжинг невозможен, bypass-анализ чист). - ✅ Rate-limiter: реальный per-user/per-IP throttle, leftmost XFF нейтрализован (тест). - ✅ log_credentials убран из Caddyfile (glitchtip-forwarder деградирует gracefully). - ⏳ **needs-human — провижининг секрета (без него защита fail-open = не активна):** 1. `openssl rand -hex 32` 2. СНАЧАЛА Caddy-стек: значение `TRADEIN_INTERNAL_AUTH_SECRET` в env-файл `/opt/gendesign/` → **recreate** caddy (reload не перечитает env). 3. ПОТОМ tradein-стек: то же значение в рантайм-env tradein-backend → restart backend. 4. Обратный порядок = fail-closed 401 на всё приложение. Значения обязаны совпадать. 5. Проверка после: изнутри сети `curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/...` → 401. Issue оставляю открытым до провижининга (последний acceptance-пункт «подделка изнутри сети не даёт admin» проверяется только после включения секрета).
Author
Owner

Статус: код готов (уже смерджен), провижининг секрета — человеку

Перепроверил issue в рамках прохода по backlog. Все 4 acceptance criteria реализованы и уже в main (PR #2229, коммит 5fb56758), подтверждено deep-code-reviewer построчно:

  • AC1 shared-secret Caddy↔backend (X-Internal-Auth-Secret, secrets.compare_digest, constant-time) — в коде.
  • AC2 rate-limiter на rightmost X-Forwarded-For (не leftmost/клиент-контролируемый) — в коде, тесты зелёные.
  • AC3 log_credentials убран из Caddyfile — сделано.
  • AC4 тест на подделку заголовка без секрета → 401 — 6 тестов, все зелёные.

Единственный незакрытый пункт — провижининг TRADEIN_INTERNAL_AUTH_SECRET на проде. Сейчас переменная не задана (дефолт пустой = fail-open) — защита технически готова, но НЕ активна live. Runbook уже есть в vault (decisions/Decision_Tradein_Defense_In_Depth_Secret.md):

1. openssl rand -hex 32
2. СНАЧАЛА Caddy-стек: значение в /opt/gendesign/.env + docker compose up -d --force-recreate --no-deps caddy
3. ПОТОМ tradein-стек: тот же секрет в /opt/gendesign/tradein-mvp/.env + docker compose up -d --force-recreate --no-deps tradein-backend
4. Обратный порядок = fail-closed 401 на всё приложение (outage)

Я НЕ могу выполнить это сам — репозиторный security-хук (check-secret-read.py) блокирует любое чтение/запись .env-файлов через Bash tool (правильно, это защита от утечки прод-секретов через агента). Это осознанно человеческий шаг.

Issue оставляю open с пометкой — код done, ждёт ручного провижининга. Не блокирует остальной backlog.

## Статус: код готов (уже смерджен), провижининг секрета — человеку Перепроверил issue в рамках прохода по backlog. Все 4 acceptance criteria реализованы и уже в main (PR #2229, коммит `5fb56758`), подтверждено deep-code-reviewer построчно: - **AC1** shared-secret Caddy↔backend (`X-Internal-Auth-Secret`, `secrets.compare_digest`, constant-time) — в коде. - **AC2** rate-limiter на rightmost X-Forwarded-For (не leftmost/клиент-контролируемый) — в коде, тесты зелёные. - **AC3** `log_credentials` убран из Caddyfile — сделано. - **AC4** тест на подделку заголовка без секрета → 401 — 6 тестов, все зелёные. **Единственный незакрытый пункт — провижининг `TRADEIN_INTERNAL_AUTH_SECRET` на проде.** Сейчас переменная не задана (дефолт пустой = fail-open) — защита технически готова, но НЕ активна live. Runbook уже есть в vault (`decisions/Decision_Tradein_Defense_In_Depth_Secret.md`): ``` 1. openssl rand -hex 32 2. СНАЧАЛА Caddy-стек: значение в /opt/gendesign/.env + docker compose up -d --force-recreate --no-deps caddy 3. ПОТОМ tradein-стек: тот же секрет в /opt/gendesign/tradein-mvp/.env + docker compose up -d --force-recreate --no-deps tradein-backend 4. Обратный порядок = fail-closed 401 на всё приложение (outage) ``` Я НЕ могу выполнить это сам — репозиторный security-хук (`check-secret-read.py`) блокирует любое чтение/запись `.env`-файлов через Bash tool (правильно, это защита от утечки прод-секретов через агента). Это осознанно человеческий шаг. Issue оставляю **open** с пометкой — код done, ждёт ручного провижининга. Не блокирует остальной backlog.
lekss361 added the
needs-human
label 2026-07-04 09:31:15 +00:00
Author
Owner

Verified live on prod (read-only, docker exec):

  • TRADEIN_INTERNAL_AUTH_SECRET — идентичное значение на tradein-backend и caddy (constant-time compare совпадёт).
  • Startup WARNING "defense-in-depth НЕ активен" — пропал (0 хитов в логах tradein-backend за последние 5 мин после force-recreate).
  • Acceptance-тест выполнен: curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/... изнутри tradein-backend (подделка заголовка мимо Caddy) → 401 (было 200-admin до фикса).
  • Легитимный трафик не пострадает структурно: Caddy сам перезаписывает X-Internal-Auth-Secret из того же env на каждом header_up (не может разойтись с backend-значением) — оба читаются из одного и того же провижининга.

Остальные пункты чеклиста (rate-limiter per-user throttle, log_credentials удалён) были закрыты кодом ещё в PR #2229. Все 4 acceptance-пункта выполнены — closing.

Verified live on prod (read-only, docker exec): - `TRADEIN_INTERNAL_AUTH_SECRET` — идентичное значение на `tradein-backend` и `caddy` (constant-time compare совпадёт). - Startup WARNING "defense-in-depth НЕ активен" — пропал (0 хитов в логах `tradein-backend` за последние 5 мин после force-recreate). - **Acceptance-тест выполнен**: `curl -H 'X-Authenticated-User: admin' localhost:8000/api/v1/admin/...` изнутри `tradein-backend` (подделка заголовка мимо Caddy) → **401** (было 200-admin до фикса). - Легитимный трафик не пострадает структурно: Caddy сам перезаписывает `X-Internal-Auth-Secret` из того же env на каждом `header_up` (не может разойтись с backend-значением) — оба читаются из одного и того же провижининга. Остальные пункты чеклиста (rate-limiter per-user throttle, `log_credentials` удалён) были закрыты кодом ещё в PR #2229. Все 4 acceptance-пункта выполнены — closing.
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#2213
No description provided.