fix(security): log_credentials + auth_audit.log → username из 401
- 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).
This commit is contained in:
parent
5e3ba351a0
commit
6f36f298ec
4 changed files with 70 additions and 13 deletions
22
Caddyfile
22
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue