- SHARED_SECRET now read from env and fail-closed: unset/placeholder refuses ALL plugin connections (constant-time compare). The old hardcoded 'your_shared_secret' in this public repo was no auth at all. Dockerfile default removed; generate_data.py reads the env var. - SECRET_KEY fails closed at startup (main.py and agent/auth.py) instead of falling back to a publicly-known signing key; agent systemd unit now requires /etc/overlord/agent.env (no '-' prefix). - AuthMiddleware + /ws/live: replace the 172.x source-IP trust (which every nginx-proxied internet request satisfied via docker-proxy — full session bypass and unauthenticated in-game command injection) with private-source AND no X-Forwarded-For, i.e. only genuinely internal callers (overlord-agent on the host, compose-network services). Invariant documented in nginx/overlord.conf: every tracker-bound location must set X-Forwarded-For. - /character-stats/test endpoints gated behind admin (they upsert real rows). - docker-compose: bind 5432/5433 to 127.0.0.1 (both DBs were internet- reachable; active brute-force observed in dereth-db logs). - discord-rare-monitor: drop dead SHARED_SECRET constant. - scripts/backup-databases.sh + docs/backups.md: nightly pg_dump of both DBs (telemetry/spawn hypertable data excluded), 10MB canary, umask 077, TimescaleDB restore procedure. - Remove stray mangled-path css file from repo root. Adversarially reviewed pre-deploy (3-lens workflow): ship verdict; deploy- sequencing blockers addressed (secret staged before enforcement, exec bit set, cron uses bash). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58 lines
2 KiB
Python
58 lines
2 KiB
Python
"""Session-cookie validation that mirrors main.py.
|
|
|
|
Re-implements the verify path so this host-side service can authenticate
|
|
the same browser cookie that dereth-tracker issues. Both services must
|
|
share the SECRET_KEY env var.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from fastapi import HTTPException, Request, status
|
|
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
|
|
|
|
# Mirror main.py — and fail closed like it does: starting with a known
|
|
# default key would let anyone forge a valid session cookie.
|
|
SECRET_KEY = os.getenv("SECRET_KEY", "")
|
|
if not SECRET_KEY or SECRET_KEY == "change-me-in-production-please":
|
|
raise RuntimeError(
|
|
"SECRET_KEY env var must be set (shared with dereth-tracker; see "
|
|
"/etc/overlord/agent.env) — refusing to start with a forgeable "
|
|
"session-signing key"
|
|
)
|
|
SESSION_MAX_AGE = 30 * 24 * 3600 # 30 days
|
|
_serializer = URLSafeTimedSerializer(SECRET_KEY)
|
|
|
|
|
|
def verify_session_cookie(token: str) -> dict | None:
|
|
"""Verify and decode a session token. Returns None if invalid/expired.
|
|
|
|
Mirrors main.py:1013-1019 byte-for-byte so a cookie issued by the tracker
|
|
decodes here identically.
|
|
"""
|
|
try:
|
|
data = _serializer.loads(token, max_age=SESSION_MAX_AGE)
|
|
return {"username": data["u"], "is_admin": data["a"]}
|
|
except (BadSignature, SignatureExpired, KeyError):
|
|
return None
|
|
|
|
|
|
def require_user(request: Request) -> dict:
|
|
"""FastAPI dependency: enforces a valid session cookie.
|
|
|
|
Returns the decoded user dict on success; raises 401 otherwise.
|
|
"""
|
|
token = request.cookies.get("session")
|
|
if not token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Not authenticated",
|
|
)
|
|
user = verify_session_cookie(token)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Session invalid or expired",
|
|
)
|
|
return user
|