fix(security): mandatory cron secret + reject placeholder JWT secrets (SEC-07, OPS-02, SEC-09)

SEC-07 — committed secrets:
- Replace real-looking defaults in .env.example (JWT_SECRET/JWT_REFRESH_SECRET
  "changeme", MANUAL_INVITE_PASSWORD "pm.local@zghl.de") with CHANGE_ME
  placeholders, and drop the personal DOME_LIGHT_WEBHOOK_URL default.
- Add a Nitro startup plugin (server/plugins/validate-secrets.ts) that refuses
  to boot in production when JWT_SECRET is unset, looks like a placeholder, or
  is shorter than 32 chars (warns only in development).

OPS-02 / SEC-09 — cron endpoints:
- requireCronSecret now fails closed: when no CRON_SECRET/KPI_CRON_SECRET is
  configured the endpoint returns 503 instead of being publicly callable
  (previously it allowed the request with a warning). Both cron routes already
  call the guard. Prefer the x-cron-secret header over the loggable ?secret=
  query param; document CRON_SECRET in .env.example.

Operational note: production deployments must now set JWT_SECRET (>=32 chars)
and CRON_SECRET, or the server won't start / crons return 503.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leubeem
2026-06-17 09:37:45 +02:00
parent 3ea297f050
commit 6b5e8b9df8

View File

@@ -3,8 +3,11 @@ NODE_ENV=development
MONGODB_URI=mongodb://127.0.0.1:27017/opensquawk
# Authentication
JWT_SECRET=changeme
JWT_REFRESH_SECRET=changeme
# Generate strong, unique random values — e.g. `openssl rand -hex 32`.
# The server refuses to start in production if these are unset, look like a
# placeholder, or are shorter than 32 characters.
JWT_SECRET=CHANGE_ME
JWT_REFRESH_SECRET=CHANGE_ME
# OpenAI
OPENAI_API_KEY=sk-your-openai-key
@@ -23,7 +26,8 @@ USE_PIPER=false
PIPER_PORT=5001
SPEACHES_BASE_URL=
SPEECH_MODEL_ID=speaches-ai/piper-en_US-ryan-low
DOME_LIGHT_WEBHOOK_URL=https://home.io.faktorxmensch.com/api/webhook/lidl_stab_3modi_8492
# Optional: external webhook for bridge dome-light telemetry. Leave empty to disable.
DOME_LIGHT_WEBHOOK_URL=
# Notifications
NOTIFY_RESEND_API_KEY=
@@ -39,4 +43,10 @@ NOTIFY_SMTP_PASS=
BOOTSTRAP_INVITE_DEADLINE=2025-09-01T00:00:00Z
# Manual invitation generator
MANUAL_INVITE_PASSWORD=pm.local@zghl.de
MANUAL_INVITE_PASSWORD=CHANGE_ME
# Cron / scheduled tasks
# Required for the /api/service/cron/* endpoints (they send emails and mint
# invite codes). Without it those endpoints return 503. Pass it via the
# `x-cron-secret` header (preferred) or `?secret=` query param.
CRON_SECRET=CHANGE_ME