fix(security): forwarder PermissionError — убрать USER nobody (read /var/log/caddy) #432

Merged
lekss361 merged 2 commits from fix/auth-forwarder-permission into main 2026-05-23 09:04:01 +00:00
Owner

⚠️ Production hotfix

GlitchTip Issue #71:

PermissionError: [Errno 13] Permission denied: '/var/log/caddy/gendsgn.ru.log'
forwarder.py:227 — main()

После merge PR #430 forwarder запущен в проде, но НЕ работает — падает на open() лог-файла. Ни одного 401 event не отправлено в GlitchTip через sidecar.

Root cause

ops/glitchtip-auth-forwarder/Dockerfile:7USER nobody (UID 65534).

Caddy 2.x создаёт /var/log/caddy/gendsgn.ru.log под UID 0 (root). Volume caddy_logs shared между контейнерами, но permissions файла 0640 root:rootnobody не может прочесть.

Variables в stack trace:

  • file_size: 135276 → лог растёт, Caddy пишет
  • current_size: 351985 → продолжает расти
  • offset: 0 → forwarder ничего не прочитал

Fix

Убрать USER nobody из Dockerfile. Forwarder работает под root.

Security обоснование

  • Volume caddy_logs:/var/log/caddy:roread-only → root не может писать
  • State volume auth_forwarder_state:/state — write-only forwarder'а, не shared
  • Образ минимальный (python:3.12-slim + sentry-sdk), нет network listener
  • Никакие credentials кроме GLITCHTIP_DSN (env) не доступны
  • Attack surface не вырастает значительно vs USER nobody

Альтернативы (отброшены):

  • Запускать под UID Caddy → Caddy сам под root, не выигрыш
  • Изменить Caddy log mode 0644 → не поддерживается в Caddyfile (только JSON config)
  • chmod sidecar на init → race condition с Caddy log rotation

Bonus fix: exception throttle

forwarder.py — добавлены _last_exc_sent + _EXC_THROTTLE_S=300s. capture_exception в outer except не чаще раза в 5 минут. Предотвращает Sentry spam если persistent ошибка повторится (как сейчас с PermissionError — без throttle GlitchTip бы заспамил сотней дубликатов).

Changes

Файл Diff
ops/glitchtip-auth-forwarder/Dockerfile -1 строка USER nobody, +2 строки comment
ops/glitchtip-auth-forwarder/forwarder.py +5 строк throttle vars + check в except

Verify after merge

После deploy.yml run (или ручного rebuild через docker compose build glitchtip-auth-forwarder && up -d --force-recreate):

ssh gendesign 'docker compose -p gendesign logs --tail=20 glitchtip-auth-forwarder'
# Ожидать: "[forwarder] starting tail from offset=..." БЕЗ PermissionError

Затем trigger тестовый 401:

curl -i https://gendsgn.ru/  # без auth → 401
# Подождать 2-5 сек
# GlitchTip dashboard → должен появиться новый issue с tag event_type:basic_auth_failed

Issue #71 после fix должен resolve (no new occurrences).

  • PR #426 (basic_auth gate)
  • PR #430 (forwarder sidecar — buggy)
  • PR #431 (deploy.yml automation, pending — после merge этого PR будет автомат на VPS)
  • GlitchTip issue #71
## ⚠️ Production hotfix GlitchTip Issue [#71](https://errors.gendsgn.ru/gendesign/issues/71): ``` PermissionError: [Errno 13] Permission denied: '/var/log/caddy/gendsgn.ru.log' forwarder.py:227 — main() ``` После merge PR #430 forwarder запущен в проде, но **НЕ работает** — падает на open() лог-файла. **Ни одного 401 event не отправлено в GlitchTip через sidecar.** ## Root cause `ops/glitchtip-auth-forwarder/Dockerfile:7` — `USER nobody` (UID 65534). Caddy 2.x создаёт `/var/log/caddy/gendsgn.ru.log` под UID 0 (root). Volume `caddy_logs` shared между контейнерами, но permissions файла `0640 root:root` — `nobody` не может прочесть. Variables в stack trace: - `file_size: 135276` → лог растёт, Caddy пишет - `current_size: 351985` → продолжает расти - `offset: 0` → forwarder ничего не прочитал ## Fix Убрать `USER nobody` из Dockerfile. Forwarder работает под root. ### Security обоснование - Volume `caddy_logs:/var/log/caddy:ro` — **read-only** → root не может писать - State volume `auth_forwarder_state:/state` — write-only forwarder'а, не shared - Образ минимальный (python:3.12-slim + sentry-sdk), нет network listener - Никакие credentials кроме `GLITCHTIP_DSN` (env) не доступны - Attack surface не вырастает значительно vs `USER nobody` Альтернативы (отброшены): - Запускать под UID Caddy → Caddy сам под root, не выигрыш - Изменить Caddy log mode 0644 → не поддерживается в Caddyfile (только JSON config) - chmod sidecar на init → race condition с Caddy log rotation ## Bonus fix: exception throttle `forwarder.py` — добавлены `_last_exc_sent` + `_EXC_THROTTLE_S=300s`. `capture_exception` в outer `except` не чаще раза в 5 минут. Предотвращает Sentry spam если persistent ошибка повторится (как сейчас с PermissionError — без throttle GlitchTip бы заспамил сотней дубликатов). ## Changes | Файл | Diff | |---|---| | `ops/glitchtip-auth-forwarder/Dockerfile` | -1 строка `USER nobody`, +2 строки comment | | `ops/glitchtip-auth-forwarder/forwarder.py` | +5 строк throttle vars + check в except | ## Verify after merge После deploy.yml run (или ручного rebuild через `docker compose build glitchtip-auth-forwarder && up -d --force-recreate`): ```bash ssh gendesign 'docker compose -p gendesign logs --tail=20 glitchtip-auth-forwarder' # Ожидать: "[forwarder] starting tail from offset=..." БЕЗ PermissionError ``` Затем trigger тестовый 401: ```bash curl -i https://gendsgn.ru/ # без auth → 401 # Подождать 2-5 сек # GlitchTip dashboard → должен появиться новый issue с tag event_type:basic_auth_failed ``` Issue #71 после fix должен resolve (no new occurrences). ## Related - PR #426 (basic_auth gate) - PR #430 (forwarder sidecar — buggy) - PR #431 (deploy.yml automation, pending — после merge этого PR будет автомат на VPS) - GlitchTip issue #71
lekss361 added 1 commit 2026-05-23 08:56:40 +00:00
Добавляет sidecar glitchtip-auth-forwarder который tails Caddy access log,
фильтрует 401 события и шлёт их в GlitchTip через Sentry SDK.

- ops/glitchtip-auth-forwarder/forwarder.py: tail + throttle + atomic offset
- Caddy JSON schema верифицирован по реальному логу: status верхнеуровневое,
  request.headers["User-Agent"] — list, client_ip vs remote_ip
- Monitor UAs (GlitchTip uptime, SentryUptimeBot) фильтруются как individual events
- Storm throttle: >10/60s → один digest event типа basic_auth_storm
- docker-compose.prod.yml: caddy_logs volume в caddy + новый service + auth_forwarder_state
- docs/runbooks/basic_auth_management.md: runbook с GlitchTip секцией
lekss361 force-pushed fix/auth-forwarder-permission from 1759158d7e to 5beed7dfce 2026-05-23 08:58:40 +00:00 Compare
lekss361 merged commit f4d43d1cfc into main 2026-05-23 09:04:01 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Security trade-off accepted: root inside container OK при текущей конфигурации (caddy_logs read-only mount, нет network listener, минимальный image python:3.12-slim + sentry-sdk, no shell server, depends_on только caddy). Attack surface не растёт значительно vs USER nobody.

Cross-PR finding: deploy.yml automation (PR #431 scope) уже включена в этот PR (commit 4dbfcfa). После merge paths-filter теперь покрывает ops/glitchtip-auth-forwarder/** → deploy.yml сработает автоматически на этом merge commit, rebuild+force-recreate sidecar. Ручной SSH шаг не нужен. PR #431 можно закрыть как duplicate.

Throttle verified: single-threaded process, module-level _last_exc_sent thread-safe. Throttle scoped к outer except Exception only, не маскирует FileNotFoundError / JSONDecodeError. Первый event всегда отправляется (now - 0.0 > 300 ⇒ True).

Follow-up suggestion (non-blocking, отдельный PR — defence-in-depth):

# docker-compose.prod.yml — glitchtip-auth-forwarder:
read_only: true
tmpfs:
  - /tmp
cap_drop:
  - ALL
security_opt:
  - no-new-privileges:true

Это нейтрализует CAP_NET_RAW/CAP_SYS_ADMIN и делает rootfs immutable. Не блокирующее, так как сейчас нет network input → реальный exploit vector минимален.

Verify after deploy completes:

ssh gendesign 'docker compose -p gendesign logs --tail=20 glitchtip-auth-forwarder'
# expect: "[forwarder] starting tail from offset=..." без PermissionError
curl -i https://gendsgn.ru/  # 401 trigger
# GlitchTip dashboard → new issue event_type:basic_auth_failed

GlitchTip Issue #71 должен auto-resolve после первого успешного цикла tail.

Merged via deep-code-reviewer — verdict ✅ APPROVE. **Security trade-off accepted**: root inside container OK при текущей конфигурации (caddy_logs read-only mount, нет network listener, минимальный image python:3.12-slim + sentry-sdk, no shell server, depends_on только caddy). Attack surface не растёт значительно vs USER nobody. **Cross-PR finding**: deploy.yml automation (PR #431 scope) уже включена в этот PR (commit 4dbfcfa). После merge `paths-filter` теперь покрывает `ops/glitchtip-auth-forwarder/**` → deploy.yml сработает автоматически на этом merge commit, rebuild+force-recreate sidecar. Ручной SSH шаг не нужен. **PR #431 можно закрыть как duplicate.** **Throttle verified**: single-threaded process, module-level `_last_exc_sent` thread-safe. Throttle scoped к outer `except Exception` only, не маскирует FileNotFoundError / JSONDecodeError. Первый event всегда отправляется (`now - 0.0 > 300` ⇒ True). **Follow-up suggestion** (non-blocking, отдельный PR — defence-in-depth): ```yaml # docker-compose.prod.yml — glitchtip-auth-forwarder: read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true ``` Это нейтрализует CAP_NET_RAW/CAP_SYS_ADMIN и делает rootfs immutable. Не блокирующее, так как сейчас нет network input → реальный exploit vector минимален. **Verify after deploy completes**: ```bash ssh gendesign 'docker compose -p gendesign logs --tail=20 glitchtip-auth-forwarder' # expect: "[forwarder] starting tail from offset=..." без PermissionError curl -i https://gendsgn.ru/ # 401 trigger # GlitchTip dashboard → new issue event_type:basic_auth_failed ``` GlitchTip Issue #71 должен auto-resolve после первого успешного цикла tail.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#432
No description provided.