fix(security): forwarder PermissionError — убрать USER nobody + deploy automation (#432)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / deploy (push) Successful in 59s
Deploy / build-backend (push) Successful in 27s
Deploy / build-worker (push) Successful in 27s
Deploy / build-frontend (push) Successful in 25s

Hotfix для GlitchTip Issue #71 (PermissionError при чтении /var/log/caddy/gendsgn.ru.log).

Changes:
- ops/glitchtip-auth-forwarder/Dockerfile — убран USER nobody, форвардер под root
- ops/glitchtip-auth-forwarder/forwarder.py — throttle capture_exception (300s)
- .forgejo/workflows/deploy.yml — paths-filter + build + force-recreate sidecar
- docs/runbooks/basic_auth_management.md — Deploy section

Security: caddy_logs mount read-only, нет network listener, нет shell,
auth_forwarder_state dedicated volume. Attack surface минимален.

После merge deploy.yml автоматически rebuild+recreate sidecar на VPS.
This commit is contained in:
lekss361 2026-05-23 09:04:00 +00:00
parent 03dc8e6735
commit f4d43d1cfc
4 changed files with 48 additions and 7 deletions

View file

@ -14,6 +14,7 @@ on:
- "Caddyfile"
- ".forgejo/workflows/deploy.yml"
- "data/sql/**"
- "ops/glitchtip-auth-forwarder/**"
workflow_dispatch:
concurrency:
@ -248,6 +249,11 @@ jobs:
done
echo "All migrations applied."
# Build local-only sidecar images (glitchtip-auth-forwarder).
# Эти services не в GHCR — сборка происходит на VPS на каждом deploy.
# Cache-friendly: первый build ~30s, последующие 1-3s если файлы не менялись.
docker compose -p gendesign -f docker-compose.prod.yml build glitchtip-auth-forwarder
docker compose -p gendesign -f docker-compose.prod.yml up -d
# backend/.env.runtime изменения (SENTRY_RELEASE, GLITCHTIP_DSN)
@ -264,6 +270,12 @@ jobs:
docker compose -p gendesign -f docker-compose.prod.yml up -d \
--force-recreate --no-deps caddy
# Forwarder: force-recreate чтобы новый image / новые env подхватывались.
# Без --force-recreate обычный `up -d` НЕ recreate'ит при image rebuild
# (т.к. image:latest tag не сменился — docker не видит разницы).
docker compose -p gendesign -f docker-compose.prod.yml up -d \
--force-recreate --no-deps glitchtip-auth-forwarder
# Cleanup old images
for repo in ghcr.io/lekss361/gendesign-backend \
ghcr.io/lekss361/gendesign-worker \

View file

@ -67,10 +67,30 @@ ssh gendesign 'docker compose -p gendesign restart glitchtip-auth-forwarder'
|---|---|---|
| `GLITCHTIP_DSN` | Sentry DSN GlitchTip (project backend) | Да |
## Phase 2 — реализовано (PR feat/glitchtip-auth-forwarder)
## Deploy
Sidecar собирается локально на VPS на каждом запуске `deploy.yml` (после merge в main).
Не требует pre-push image build, не использует GHCR.
Forwarder контейнер пересоздаётся `--force-recreate` чтобы подхватить новый image при изменении кода.
В случае sidecar не запустился:
```bash
ssh gendesign 'docker compose -p gendesign -f docker-compose.prod.yml logs --tail=100 glitchtip-auth-forwarder'
```
Если build fail на VPS — проверь `ops/glitchtip-auth-forwarder/Dockerfile` локально:
```bash
docker build ops/glitchtip-auth-forwarder/
```
## Phase 2 — реализовано (PR #430)
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)

View file

@ -7,8 +7,9 @@ RUN pip install --no-cache-dir "sentry-sdk==2.18.0"
COPY forwarder.py .
# nobody (uid 65534) — минимальные привилегии
USER nobody
# Запускается под root — необходимо для чтения /var/log/caddy/gendsgn.ru.log
# (Caddy пишет лог под UID 0). Mounts: caddy_logs:ro + auth_forwarder_state (write).
# Attack surface минимален: нет network listener, нет shell, образ python:3.12-slim.
# -u для unbuffered stdout — docker logs виден сразу без буферизации
CMD ["python", "-u", "forwarder.py"]

View file

@ -67,6 +67,10 @@ KNOWN_MONITOR_UAS = frozenset(
)
_shutdown = False
# Throttle для unhandled exceptions — не более одного Sentry event за 5 минут,
# чтобы persistent ошибка (напр. PermissionError) не спамила GlitchTip.
_last_exc_sent: float = 0.0
_EXC_THROTTLE_S: float = 300.0
def _signal_handler(signum: int, frame: object) -> None:
@ -299,10 +303,14 @@ def main() -> None:
time.sleep(5)
except Exception as exc:
print(f"[forwarder] unhandled error: {exc}", file=sys.stderr, flush=True)
try:
sentry_sdk.capture_exception(exc)
except Exception:
pass
global _last_exc_sent
now_wall = time.time()
if now_wall - _last_exc_sent > _EXC_THROTTLE_S:
try:
sentry_sdk.capture_exception(exc)
except Exception:
pass
_last_exc_sent = now_wall
time.sleep(5)
print("[forwarder] shutting down gracefully", flush=True)