fix(caddy): снять мёртвый internal-секрет с анонимного пути и чанки закрытого контура с публичного домена
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
#3324, п.3: Caddy на gendsgn.ru инжектил X-Internal-Auth-Secret в каждый анонимный запрос /trade-in/api/* и /trade-in/* (хоп во фронт). Сравнение недостижимо по построению: backend читает этот заголовок только в legacy trusted-header ветке rbac.py, и только после непустого X-Authenticated-User, который тот же самый хоп строкой ниже удаляет. Второй потребитель секрета (вебхук GlitchTip) берёт его из query-параметра. Защита от подделки заголовков внутри gendesign_shared — проверка в rbac.py, она не тронута; инжект работал в обратную сторону, раздавая секрет наружу. #3324, п.1: meraocenka.ru раздавал анонимам постраничные чанки App Router чужих маршрутов (chunks/app/{admin,team,scrapers,...}/) — код закрытого контура с именами внутренних ручек. Матчер отсекает 404-ом «есть подкаталог, и он не mera-public»; лэндингу нужны только chunks/app/<файл>.js и chunks/app/mera-public/* (замер на проде). Карта чанков в webpack-*.js остаётся — имена файлов по-прежнему читаются, полное разделение = split build. Регресс-тесты в smoke-mera-perimeter.sh отличают 404 Caddy (пустое тело) от 404 Next и отдельно проверяют, что статика лэндинга жива.
This commit is contained in:
parent
d8b7de2cf6
commit
177207223b
2 changed files with 105 additions and 10 deletions
|
|
@ -175,10 +175,28 @@ gendsgn.ru {
|
|||
# "удалить заголовок" (Caddyfile reverse_proxy directive: `-<field>` =
|
||||
# delete) — корректное поведение не должно зависеть от того, как именно
|
||||
# Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции.
|
||||
# X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается
|
||||
# из env (Set-операция с непустым значением, никак не связана с auth-гейтом
|
||||
# basic_auth), это единственное, что теперь отсекает подделку заголовков
|
||||
# изнутри gendesign_shared network для legacy dual-mode пути.
|
||||
# X-Internal-Auth-Secret ЗДЕСЬ БОЛЬШЕ НЕ ПОДСТАВЛЯЕТСЯ (#3324). До этой
|
||||
# правки Caddy инжектил его в КАЖДЫЙ запрос этого хопа — включая
|
||||
# анонимный, до всякого логина trade-in.
|
||||
#
|
||||
# ПОЧЕМУ ОН БЫЛ МЁРТВЫМ. Единственное место, где backend читает этот
|
||||
# ЗАГОЛОВОК, — `app/core/rbac.py` (legacy trusted-header ветка): он
|
||||
# сверяется ТОЛЬКО после того, как в запросе нашёлся непустой
|
||||
# X-Authenticated-User (иначе ветка отдаёт 401 раньше, на «no
|
||||
# authenticated user»). А X-Authenticated-User на этом же хопе строкой
|
||||
# ниже удаляется — то есть пара «имя + секрет» через Caddy прийти не
|
||||
# может по построению, сравнение недостижимо. Второй потребитель
|
||||
# секрета, приёмник вебхуков GlitchTip (`app/api/v1/glitchtip.py`),
|
||||
# берёт его из query-параметра `?secret=`, а не из заголовка, и на эту
|
||||
# правку не реагирует.
|
||||
#
|
||||
# ЧТО ОТСЕКАЕТ ПОДДЕЛКУ ЗАГОЛОВКОВ ИЗНУТРИ gendesign_shared — проверка
|
||||
# в rbac.py, а не факт подстановки в Caddy; она остаётся нетронутой.
|
||||
# Инжект работал в обратную сторону: раздавал внутренний секрет по
|
||||
# анонимному пути (он же виден в env контейнера Caddy). Легитимный
|
||||
# dual-mode трафик — внутрисетевой (`docker exec tradein-backend curl
|
||||
# -H 'X-Authenticated-User: …' -H 'X-Internal-Auth-Secret: …'`,
|
||||
# см. auth/roles.yaml) — ходит мимо Caddy и сам несёт оба заголовка.
|
||||
handle /trade-in/api/* {
|
||||
# `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api;
|
||||
# FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только
|
||||
|
|
@ -186,7 +204,6 @@ gendsgn.ru {
|
|||
uri strip_prefix /trade-in
|
||||
reverse_proxy tradein-backend:8000 {
|
||||
header_up -X-Authenticated-User
|
||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,9 +241,13 @@ gendsgn.ru {
|
|||
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
||||
reverse_proxy tradein-frontend:3000 {
|
||||
# См. комментарий над /trade-in/api/* выше — та же логика (явное
|
||||
# удаление вместо Set с пустым {http.auth.user.id}).
|
||||
# удаление вместо Set с пустым {http.auth.user.id}), и по той же
|
||||
# причине здесь больше нет инжекта X-Internal-Auth-Secret
|
||||
# (#3324). На этом хопе он был мёртв ещё очевиднее: адресат —
|
||||
# Next-сервер tradein-frontend, в его коде заголовок не читается
|
||||
# и дальше в backend не пробрасывается (SSR не форвардит
|
||||
# входящие заголовки — `headers()` во фронте не используется).
|
||||
header_up -X-Authenticated-User
|
||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -463,7 +484,33 @@ meraocenka.ru {
|
|||
# `_next/*` открыл бы анонимам ещё и `/_next/image` (оптимизация картинок,
|
||||
# CPU-нагрузка по запросу), который на лэндинге не используется вообще:
|
||||
# next/image в tradein-mvp/frontend/src/app/mera-public/ не импортируется.
|
||||
#
|
||||
# #3324: внутри разрешённой статики закрыто ПОДДЕРЕВО ЧУЖИХ МАРШРУТОВ.
|
||||
# App Router раскладывает код постранично: `chunks/app/<маршрут>/page-<hash>.js`
|
||||
# (замер на проде 02.09.2026 — лэндингу нужны РОВНО `chunks/app/layout-*`,
|
||||
# `chunks/app/error-*`, `chunks/app/global-error-*`, `chunks/app/mera-public/*`
|
||||
# и общие хэш-чанки прямо в `chunks/`). Всё остальное под `chunks/app/` —
|
||||
# это /admin, /scrapers, /team, /history, /cache, /versions, /v2,
|
||||
# /sale-share, /login, /ui-preview: страницы закрытого контура, которые
|
||||
# аноним с публичного домена качал целиком и читал в них имена внутренних
|
||||
# ручек. Матчер отсекает ровно «есть подкаталог, и он не mera-public» —
|
||||
# `not` вместо негативного lookahead, RE2 его не поддерживает.
|
||||
#
|
||||
# Ограничение честно: это убирает КОД, но не имена файлов — карта чанков
|
||||
# лежит в `webpack-*.js`, который лэндингу нужен, поэтому перечень
|
||||
# маршрутов из неё по-прежнему вычитывается. Полное разделение — split
|
||||
# build (отдельный public-бандл), отдельная задача.
|
||||
#
|
||||
# `respond` внутри `handle` выполняется раньше `reverse_proxy` (порядок
|
||||
# директив Caddy), поэтому вложенный матчер не зависит от сортировки
|
||||
# handle-блоков между собой.
|
||||
handle /trade-in/_next/static/* {
|
||||
@foreignRouteChunk {
|
||||
path_regexp ^/trade-in/_next/static/chunks/app/[^/]+/
|
||||
not path /trade-in/_next/static/chunks/app/mera-public/*
|
||||
}
|
||||
respond @foreignRouteChunk 404
|
||||
|
||||
reverse_proxy tradein-frontend:3000 {
|
||||
header_up -X-Authenticated-User
|
||||
}
|
||||
|
|
@ -499,9 +546,12 @@ meraocenka.ru {
|
|||
# strip_prefix — та же причина, что у B2B-хопа (:127): basePath Next'а не
|
||||
# часть маршрута FastAPI.
|
||||
#
|
||||
# X-Internal-Auth-Secret здесь НЕ подставляется (в отличие от :130):
|
||||
# публичные ручки его не проверяют, а инжектить внутренний секрет в хоп с
|
||||
# анонимного домена — расширять доверие без нужды.
|
||||
# X-Internal-Auth-Secret здесь НЕ подставляется: публичные ручки его не
|
||||
# проверяют, а инжектить внутренний секрет в хоп с анонимного домена —
|
||||
# расширять доверие без нужды. С #3324 это уже не отличие от B2B-блока
|
||||
# gendsgn.ru, а общее правило: инжекта не осталось нигде в Caddyfile —
|
||||
# заголовок нужен только внутрисетевому dual-mode трафику, который ходит
|
||||
# мимо прокси и несёт его сам.
|
||||
handle /trade-in/api/public/* {
|
||||
uri strip_prefix /trade-in
|
||||
reverse_proxy tradein-backend:8000 {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,51 @@ check "meraocenka.ru/trade-in/api/* — must 404 (не проксируем API)
|
|||
# Ловит расширение матчера обратно до `/trade-in/_next/*`.
|
||||
check "meraocenka.ru/_next/image — must 404 (не открываем оптимизатор)" "$BASE_MERA/trade-in/_next/image?url=%2Ftest.png&w=64&q=75" 404
|
||||
|
||||
# 2c-bis. Внутри разрешённой статики закрыто поддерево ЧУЖИХ маршрутов (#3324):
|
||||
# App Router кладёт код постранично в chunks/app/<маршрут>/, и до этой
|
||||
# правки аноним скачивал с публичного домена бандлы /admin, /team,
|
||||
# /scrapers — с именами внутренних ручек внутри.
|
||||
#
|
||||
# КОД 404 ЗДЕСЬ НЕДОСТАТОЧЕН: несуществующий чанк Next тоже отдаёт 404,
|
||||
# поэтому проверка не отличила бы «Caddy отсёк» от «Caddy проксировал, а
|
||||
# файла нет» — и осталась бы зелёной после отката матчера. Отличаем по
|
||||
# ТЕЛУ: `respond 404` Caddy пустой (0 байт), 404 от Next — непустой
|
||||
# (замер на проде 02.09.2026: 9 байт).
|
||||
check_caddy_404() {
|
||||
local desc="$1" url="$2"
|
||||
local out code size
|
||||
out=$(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 15 "$url" 2>/dev/null)
|
||||
code=${out%% *}
|
||||
size=${out##* }
|
||||
if [ "$code" = "404" ] && [ "$size" = "0" ]; then
|
||||
echo "PASS: $desc ($url -> 404, пустое тело = отсёк Caddy)"
|
||||
else
|
||||
echo "FAIL: $desc ($url -> got '${out:-<no response>}', expected '404 0')"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
check_caddy_404 "meraocenka.ru — чанки /admin не раздаются" \
|
||||
"$BASE_MERA/trade-in/_next/static/chunks/app/admin/page-smoke.js"
|
||||
check_caddy_404 "meraocenka.ru — чанки /admin/analytics не раздаются" \
|
||||
"$BASE_MERA/trade-in/_next/static/chunks/app/admin/analytics/page-smoke.js"
|
||||
check_caddy_404 "meraocenka.ru — чанки /team не раздаются" \
|
||||
"$BASE_MERA/trade-in/_next/static/chunks/app/team/page-smoke.js"
|
||||
|
||||
# Обратная сторона того же матчера: статика САМОГО лэндинга обязана остаться
|
||||
# живой. Без этой строки «починка» вида «404 на весь chunks/app/» выглядела бы
|
||||
# успешной, а публичный сайт молча остался бы без JS.
|
||||
layout_chunk=$(curl -s --max-time 15 "$BASE_MERA/" 2>/dev/null \
|
||||
| grep -o '/trade-in/_next/static/chunks/app/layout-[^"]*\.js' | head -1)
|
||||
if [ -z "$layout_chunk" ]; then
|
||||
# Пустая строка вместо пути дала бы запрос к корню и зелёную проверку ни о
|
||||
# чём — поэтому это FAIL, а не «пропустим».
|
||||
echo "FAIL: не нашёл layout-чанк в HTML лэндинга (сам лэндинг сломан?)"
|
||||
fail=1
|
||||
else
|
||||
check "meraocenka.ru — корневой layout-чанк лэндинга жив (200)" "$BASE_MERA$layout_chunk" 200
|
||||
fi
|
||||
|
||||
# 2d. Публичный API МЕРЫ (#2911). Ровно две ручки под /api/public/mera/*
|
||||
# доступны анонимно на обоих доменах; ВЕСЬ /api/v1/* на публичном домене
|
||||
# по-прежнему 404.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue