From 6f36f298ecc1caf97cfada23cd566145d71eaf10 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 23 May 2026 12:51:53 +0300 Subject: [PATCH] =?UTF-8?q?fix(security):=20log=5Fcredentials=20+=20auth?= =?UTF-8?q?=5Faudit.log=20=E2=86=92=20username=20=D0=B8=D0=B7=20401?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Caddyfile: global servers { log_credentials } — Caddy перестаёт редактировать Authorization header в "REDACTED" - Caddyfile: отдельный log auth_audit { } → /var/log/caddy/auth_audit.log (retention 7d, roll 10MiB×3) для изоляции auth-событий - docker-compose.prod.yml: CADDY_LOG_FILE → auth_audit.log (forwarder читает меньший файл, снижает false reads от non-auth requests) - forwarder.py: обновлены docstrings — требования к log_credentials явные - docs/runbooks: секция «Аудит логи» + security note про log_credentials Результат: _extract_attempted_username получает raw Basic auth header (не "REDACTED") → attempted_username tag в GlitchTip event заполнен. Caddy validate: Valid configuration (caddy:2 v2.11.3). --- Caddyfile | 22 +++++++++++++ docker-compose.prod.yml | 2 +- docs/runbooks/basic_auth_management.md | 40 +++++++++++++++++++++-- ops/glitchtip-auth-forwarder/forwarder.py | 19 ++++++----- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/Caddyfile b/Caddyfile index 4902a5b1..734bfcb1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -16,6 +16,16 @@ # (basic_auth runs before handle), making /health and /preview/* exclusions # ineffective. With route { }, handlers are matched top-to-bottom as written. +{ + servers { + # ВКЛЮЧАЕТ raw Authorization/Cookie headers в access log + # (по default Caddy 2.x редактирует их как "REDACTED"). + # Plain password будет в gendsgn.ru.log + auth_audit.log — оба root-only на VPS. + # См. forwarder.py _extract_attempted_username для использования. + log_credentials + } +} + gendsgn.ru { encode zstd gzip @@ -28,6 +38,18 @@ gendsgn.ru { format json } + # Отдельный лог только для auth-событий. + # Forwarder (ops/glitchtip-auth-forwarder) читает именно этот файл. + # Retention 7 дней (меньше чем main log) — содержит plain Base64 credentials. + log auth_audit { + output file /var/log/caddy/auth_audit.log { + roll_size 10MiB + roll_keep 3 + roll_keep_for 168h + } + format json + } + route { # /health и /preview/* — public, без auth, short-circuit. handle /health { diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1c0381dd..8c77d93c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -216,7 +216,7 @@ services: GLITCHTIP_DSN: ${GLITCHTIP_DSN} APP_ENV: production APP_RELEASE: auth-forwarder-1 - CADDY_LOG_FILE: /var/log/caddy/gendsgn.ru.log + CADDY_LOG_FILE: /var/log/caddy/auth_audit.log STATE_FILE: /state/offset.json volumes: # Read-only доступ к Caddy access log diff --git a/docs/runbooks/basic_auth_management.md b/docs/runbooks/basic_auth_management.md index 4affcd50..a33e3710 100644 --- a/docs/runbooks/basic_auth_management.md +++ b/docs/runbooks/basic_auth_management.md @@ -31,11 +31,36 @@ При компрометации пароля: remove_user + add_user с новым паролем + PR-merge + разослать новый credential. +## Аудит логи + +- **Auth audit**: `/var/log/caddy/auth_audit.log` — все события basic_auth (включая raw Authorization). + Содержит plain credentials в Base64 — accessible только root на VPS. + Используется forwarder'ом (см. `ops/glitchtip-auth-forwarder/forwarder.py`). + Retention: 7 дней (roll_size 10MiB, keep 3, 168h). + +- **Access log**: `/var/log/caddy/gendsgn.ru.log` — все requests, format JSON. + Тоже содержит raw Authorization из-за global `log_credentials` (Caddy 2.x — опция только global, нельзя per-block). + Retention: 30 дней (roll_size 50MiB, keep 5, 720h). + +Failed auth последние 100: + +```bash +docker compose -f docker-compose.prod.yml exec caddy \ + grep -P '"status":401' /var/log/caddy/auth_audit.log | tail -100 | jq +``` + +### Security note + +`log_credentials` — опция под `servers { }` global block, нельзя per-virtual-host. Plain Base64 credentials попадают в **оба** log файла. Mitigation: +- VPS root-only (только SSH key authorized owner) +- Retention: gendsgn.ru.log = 30 дней, auth_audit.log = 7 дней +- Не выгружать логи на внешние системы без redaction + ## Диагностика ```bash -# Посмотреть 401-ошибки в live логе -ssh gendesign 'docker compose -p gendesign exec caddy tail -f /var/log/caddy/gendsgn.ru.log' | grep '"status":401' +# Посмотреть 401-ошибки в auth audit log +ssh gendesign 'docker compose -p gendesign exec caddy tail -f /var/log/caddy/auth_audit.log' | grep '"status":401' # Reload конфига Caddy без перезапуска ssh gendesign 'docker compose -p gendesign exec -T caddy caddy reload --config /etc/caddy/Caddyfile' @@ -92,7 +117,16 @@ GlitchTip forwarder sidecar добавлен в `ops/glitchtip-auth-forwarder/`. Архитектура: Python 3.12 slim, sentry-sdk 2.18, tail live Caddy log, offset в named volume. Deploy автоматизирован через `deploy.yml` — ручной SSH на VPS не нужен. -## Phase 3 (TODO) +## Phase 3 — реализовано (PR #436) + +`log_credentials` + `auth_audit.log` → forwarder получает реальный username из 401. + +- Global `log_credentials` в Caddyfile — Caddy перестаёт редактировать Authorization header. +- Отдельный `auth_audit.log` (retention 7d) — forwarder читает именно его. +- `_extract_attempted_username` декодирует Basic auth → `attempted_username` tag в GlitchTip event. +- Результат: GlitchTip event получает `attempted_username:testbruteforce` вместо `(none)`. + +## Phase 4 (TODO) - При >30 users — миграция на OAuth/NextAuth - Разделение admin/user по доступу через Caddy path matcher или app-level RBAC diff --git a/ops/glitchtip-auth-forwarder/forwarder.py b/ops/glitchtip-auth-forwarder/forwarder.py index 4bed2ea1..8fc3c5b4 100644 --- a/ops/glitchtip-auth-forwarder/forwarder.py +++ b/ops/glitchtip-auth-forwarder/forwarder.py @@ -1,7 +1,7 @@ """ Caddy access log -> GlitchTip auth event forwarder. -Tails /var/log/caddy/gendsgn.ru.log, фильтрует JSON lines с +Tails /var/log/caddy/auth_audit.log (CADDY_LOG_FILE env), фильтрует JSON lines с status==401, шлёт в GlitchTip через Sentry SDK как warning event с тэгами. Persistent offset в /state/offset.json — не дублируем при restart. @@ -24,7 +24,7 @@ Throttle: при >10 401 events за 60s — однократный digest event "uri": "/", "headers": { "User-Agent": ["Mozilla/5.0 ..."], - "Authorization": ["REDACTED"], + "Authorization": ["Basic dXNlcjpwYXNz"], # raw — при включённом log_credentials в Caddyfile ... }, "tls": {...} @@ -116,15 +116,16 @@ def is_monitor_ua(ua: str) -> bool: def _extract_attempted_username(entry: dict) -> str | None: # type: ignore[type-arg] """ - Извлекает username из Authorization header (Basic auth) из попытки входа. + Извлекает username из Authorization header (Basic auth) из Caddy access log. - Возвращает None если: - - Authorization header отсутствует - - Формат не Basic - - base64 не декодируется - - после decode нет ':' + Требования к Caddyfile: + - global option `log_credentials` (иначе Caddy редактирует Authorization → "REDACTED") + - этот forwarder ожидает auth_audit.log как primary source (CADDY_LOG_FILE env) - Никогда не логирует / не возвращает пароль. + Возвращает только username (всё до ':' в base64-decoded auth). + Password нигде не сохраняется, не логируется. + + None если: header отсутствует, не Basic, base64 decode fail, нет ':', > 128 chars. """ req = entry.get("request") or {} headers = req.get("headers") or {}