fix(security): применить ревью PR #426 — route wrap + log rotation + base64 busybox + runbook log fix
- 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: #426 (comment)
This commit is contained in:
parent
b89b34c300
commit
cdce8e84a1
3 changed files with 72 additions and 57 deletions
106
Caddyfile
106
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue