From cdce8e84a15dd4d664a28f09a7283ae9ae499971 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 23 May 2026 11:11:50 +0300 Subject: [PATCH] =?UTF-8?q?fix(security):=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20PR?= =?UTF-8?q?=20#426=20=E2=80=94=20route=20wrap=20+=20log=20rotation=20+=20b?= =?UTF-8?q?ase64=20busybox=20+=20runbook=20log=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Caddyfile: обернуть gendsgn.ru и :80 в route { } — сохраняет порядок директив, /health и /preview/* теперь действительно short-circuit до basic_auth - Caddyfile: добавить roll_size 50MiB / roll_keep 5 / roll_keep_for 720h в gendsgn.ru.log - scripts/auth/add_user.sh: заменить base64 -w 0 (GNU-only) на base64 | tr -d "\n" (busybox-compatible, caddy:2 Alpine base image) - docs/runbooks/basic_auth_management.md: убрать несуществующий auth_audit.log, заменить на реальную команду grep по статусу 401 в gendsgn.ru.log caddy validate: Valid configuration (confirmed locally via caddy:2 container). Ref: https://git.gendsgn.ru/lekss361/gendesign/pulls/426#issuecomment-2235 --- Caddyfile | 106 ++++++++++++++----------- docs/runbooks/basic_auth_management.md | 17 ++-- scripts/auth/add_user.sh | 6 +- 3 files changed, 72 insertions(+), 57 deletions(-) diff --git a/Caddyfile b/Caddyfile index 1dda17ff..4902a5b1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -10,56 +10,67 @@ # Auth gate: basic_auth for pilot phase (2026-05-23). # Users managed via caddy/users.caddy.snippet (git history = audit trail). # Public exclusions: /health (liveness probe), /preview/* (static mockups). +# +# IMPORTANT: route { } block is required to preserve directive order. +# Without route { }, Caddy executes directives in hard-coded default order +# (basic_auth runs before handle), making /health and /preview/* exclusions +# ineffective. With route { }, handlers are matched top-to-bottom as written. gendsgn.ru { encode zstd gzip log { - output file /var/log/caddy/gendsgn.ru.log + output file /var/log/caddy/gendsgn.ru.log { + roll_size 50MiB + roll_keep 5 + roll_keep_for 720h + } format json } - # Liveness probe — public, before auth (GHA deploy smoke check). - handle /health { - reverse_proxy backend:8000 - } + route { + # /health и /preview/* — public, без auth, short-circuit. + handle /health { + reverse_proxy backend:8000 + } - # Static HTML mockups для review (audit alternatives). - # Public access — без auth (по запросу 2026-05-17). - handle_path /preview/* { - root * /srv/preview - file_server browse - } + # Static HTML mockups для review (audit alternatives). + # Public access — без auth (по запросу 2026-05-17). + handle_path /preview/* { + root * /srv/preview + file_server browse + } - # Auth gate (applies to all routes not matched above). - import caddy/users.caddy.snippet + # Auth gate (applies to all routes below within this route block). + import caddy/users.caddy.snippet - # Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker stack, - # подключен через gendesign_shared network. Routes ДО универсального handle - # потому что Caddy матчит handle-блоки сверху вниз. - handle /trade-in/api/* { - # `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api; - # FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только - # префикса basePath /trade-in (Next.js basePath leak). - uri strip_prefix /trade-in - reverse_proxy tradein-backend:8000 - } + # Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker stack, + # подключен через gendesign_shared network. Routes ДО универсального handle + # потому что Caddy матчит handle-блоки сверху вниз. + handle /trade-in/api/* { + # `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api; + # FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только + # префикса basePath /trade-in (Next.js basePath leak). + uri strip_prefix /trade-in + reverse_proxy tradein-backend:8000 + } - # Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша), - # И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*` - # пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ. - @tradein path /trade-in /trade-in/* - handle @tradein { - # Next.js basePath=/trade-in — фронт сам ждёт префикса в URL - reverse_proxy tradein-frontend:3000 - } + # Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша), + # И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*` + # пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ. + @tradein path /trade-in /trade-in/* + handle @tradein { + # Next.js basePath=/trade-in — фронт сам ждёт префикса в URL + reverse_proxy tradein-frontend:3000 + } - handle /api/* { - reverse_proxy backend:8000 - } + handle /api/* { + reverse_proxy backend:8000 + } - handle { - reverse_proxy frontend:3000 + handle { + reverse_proxy frontend:3000 + } } } @@ -117,19 +128,22 @@ git.gendsgn.ru { :80 { encode zstd gzip - handle /health { - reverse_proxy backend:8000 - } + route { + # /health — public, без auth (GHA deploy smoke check, liveness probe). + handle /health { + reverse_proxy backend:8000 + } - # Auth gate (same snippet as gendsgn.ru). - import caddy/users.caddy.snippet + # Auth gate (same snippet as gendsgn.ru). + import caddy/users.caddy.snippet - handle /api/* { - reverse_proxy backend:8000 - } + handle /api/* { + reverse_proxy backend:8000 + } - handle { - reverse_proxy frontend:3000 + handle { + reverse_proxy frontend:3000 + } } } # Test deploy flow 2026-05-15T21:43:32Z diff --git a/docs/runbooks/basic_auth_management.md b/docs/runbooks/basic_auth_management.md index 718a1de2..9b863dfa 100644 --- a/docs/runbooks/basic_auth_management.md +++ b/docs/runbooks/basic_auth_management.md @@ -39,16 +39,13 @@ Caddy начнёт отклонять запросы этого юзера ср ## Аудит логи -- Access log: `/var/log/caddy/gendsgn.ru.log` (все запросы, JSON) -- Auth audit: `/var/log/caddy/auth_audit.log` (authentication events, JSON) - -```bash -# Просмотр auth событий в реальном времени -docker compose -f docker-compose.prod.yml exec caddy tail -f /var/log/caddy/auth_audit.log | jq - -# Посмотреть кто логинился сегодня -docker compose -f docker-compose.prod.yml exec caddy grep "$(date +%Y-%m-%d)" /var/log/caddy/auth_audit.log | jq '.user_id' -``` +- Access log: `/var/log/caddy/gendsgn.ru.log` (все запросы JSON, включая auth events). +- Failed auth последние 100: + ```bash + docker compose -f docker-compose.prod.yml exec caddy \ + grep -P '"status":401' /var/log/caddy/gendsgn.ru.log | tail -100 | jq + ``` +- Phase 2 TODO: отдельный auth_audit.log + GlitchTip forwarder sidecar. ## Caddy reload после ручной правки на VPS diff --git a/scripts/auth/add_user.sh b/scripts/auth/add_user.sh index b0584a5f..e8afa6b6 100644 --- a/scripts/auth/add_user.sh +++ b/scripts/auth/add_user.sh @@ -55,7 +55,11 @@ fi # Caddy 2.11+ requires base64-encoded bcrypt in Caddyfile basic_auth. # Password passed via stdin to avoid exposure in process list. -HASH="$(printf '%s' "$PLAIN_PASS" | docker run --rm -i caddy:2 sh -c 'read -r p; caddy hash-password --plaintext "$p" | tr -d "\n" | base64 -w 0')" +HASH="$(printf '%s' "$PLAIN_PASS" | docker run --rm -i caddy:2 sh -c ' + pass=$(cat) + raw=$(caddy hash-password --algorithm bcrypt --plaintext "$pass") + printf "%s" "$raw" | base64 | tr -d "\n" +')" if [[ -z "$HASH" ]]; then echo "ERROR: caddy hash-password returned empty hash" >&2