diff --git a/Caddyfile b/Caddyfile index c29dad19..84f137de 100644 --- a/Caddyfile +++ b/Caddyfile @@ -77,7 +77,15 @@ gendsgn.ru { # Оба ДО auth-import, иначе ассеты страницы уходят в @tradein (под auth) → 401 → без CSS. @uipreview path /trade-in/ui-preview/* /trade-in/_next/static/* handle @uipreview { - reverse_proxy tradein-frontend:3000 + reverse_proxy tradein-frontend:3000 { + # #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и + # @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент + # мог бы прислать свой X-Authenticated-User. Сейчас инертно + # (страница статична, у tradein-frontend нет секрета для + # X-Internal-Auth-Secret), но убираем ради единообразия периметра, + # а не полагаясь на то, что downstream ничего не делает с заголовком. + header_up -X-Authenticated-User + } } # #2558: Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker @@ -91,19 +99,23 @@ gendsgn.ru { # X-Authenticated-User — ЯВНОЕ УДАЛЕНИЕ (`header_up -X-Authenticated-User`), # НЕ `header_up X-Authenticated-User {http.auth.user.id}`. Причина: этот # блок больше не идёт ПОСЛЕ basic_auth, поэтому `{http.auth.user.id}` - # никогда не резолвится Caddy'ем (плейсхолдер пуст) — а поведение - # `header_up ` при пустом value НЕ задокументировано как - # надёжное удаление заголовка (это Set-операция; нет гарантии, что Caddy - # трактует пустую строку как "не отправлять поле" во всех версиях). - # Полагаться на это было бы небезопасно: если бы Caddy просто НЕ - # устанавливал заголовок (leave as-is) при пустом value, то ЛЮБОЙ - # клиентский `X-Authenticated-User: someone` прошёл бы НАСКВОЗЬ до - # tradein-backend — а backend в auth_mode="dual" (`app/core/config.py`) - # всё ещё принимает legacy trusted-header путь (`rbac.py:186`) → внешняя - # подмена личности без единого валидного пароля. `-Field` — единственная - # ФОРМА С ЯВНО ЗАДОКУМЕНТИРОВАННОЙ семантикой "удалить заголовок" - # (Caddyfile reverse_proxy directive: `-` = delete), поэтому - # используем её вместо Set с пустым значением — гарантия, а не догадка. + # никогда не резолвится авторизованным юзером на этом пути. + # Проверено эмпирически (echo-стенд на образе caddy:2, `caddy adapt`): + # старая Set-форма (`header_up X-Authenticated-User {http.auth.user.id}`) + # НЕ пропустила бы клиентский заголовок насквозь и НЕ оставила бы поле + # пустым — Caddy подставляет НЕРАЗРЕШЁННЫЙ плейсхолдер как ЛИТЕРАЛЬНУЮ + # строку (`ReplaceKnown`), т.е. upstream получил бы буквально + # `X-Authenticated-User: {http.auth.user.id}`. Для backend (auth_mode= + # "dual", `app/core/config.py`) это НЕ подмена личности — legacy path + # (`rbac.py:186`) сделал бы `get_role("{http.auth.user.id}")`, юзер не + # найден в roles.yaml → 403 для всех. Т.е. старая форма была бы не + # security-дырой, а fail-closed-but-сломанной (все trade-in запросы без + # session-cookie получали бы 403 вместо ожидаемого 401/редиректа на логин). + # `-Field` остаётся правильным выбором не потому что Set был бы дырой, а + # потому что это ЕДИНСТВЕННАЯ форма с явно задокументированной семантикой + # "удалить заголовок" (Caddyfile reverse_proxy directive: `-` = + # delete) — корректное поведение не должно зависеть от того, как именно + # Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции. # X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается # из env (Set-операция с непустым значением, никак не связана с auth-гейтом # basic_auth), это единственное, что теперь отсекает подделку заголовков @@ -124,9 +136,15 @@ gendsgn.ru { # (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL # в адресной строке требует отдельного Next-app с basePath=/sale-share. # #2558: перенесён ВЫШЕ auth-import вместе с trade-in — редирект ведёт на - # /trade-in/sale-share, который теперь публичен без basic_auth; оставлять - # короткий /sale-share под basic_auth, а канонический адрес назначения без - # него — несогласованно (короткий URL был бы строже целевой страницы). + # /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и + # для остального /trade-in). Это НЕ делает страницу публичной: она всё + # ещё за собственной авторизацией trade-in — `RouteGuard` во фронте + # (`app/layout.tsx`, «Доступ: pilot + admin») и сессия для + # `/api/v1/buildings/sale-share*` на бэке; без валидной сессии юзер + # получит редирект на /login, а не контент. Смысл переноса — не открыть + # страницу всем, а убрать несогласованность: короткий URL не должен быть + # строже (Caddy basic_auth) целевого адреса, к которому и так уже нет + # basic_auth-барьера (только собственный login trade-in). @saleshare path /sale-share /sale-share/ handle @saleshare { redir /trade-in/sale-share permanent