mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-06-28 03:15:38 +08:00
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>
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
// Startup validation for security-critical secrets. Runs once when the Nitro
|
|
// server boots. In production it fails fast (throws, refusing to start) rather
|
|
// than running with forgeable JWTs; in development it only warns so local setup
|
|
// stays frictionless.
|
|
|
|
const PLACEHOLDER_MARKERS = ['changeme', 'change_me']
|
|
|
|
function looksLikePlaceholder(value: string): boolean {
|
|
const v = value.toLowerCase()
|
|
return PLACEHOLDER_MARKERS.some((marker) => v.includes(marker))
|
|
}
|
|
|
|
export default defineNitroPlugin(() => {
|
|
const isProd = process.env.NODE_ENV === 'production'
|
|
const jwtSecret = (process.env.JWT_SECRET || '').trim()
|
|
const refreshSecret = (process.env.JWT_REFRESH_SECRET || '').trim()
|
|
|
|
const problems: string[] = []
|
|
|
|
if (!jwtSecret) {
|
|
problems.push('JWT_SECRET is not set')
|
|
} else if (looksLikePlaceholder(jwtSecret)) {
|
|
problems.push('JWT_SECRET still uses a placeholder/example value')
|
|
} else if (jwtSecret.length < 32) {
|
|
problems.push('JWT_SECRET is shorter than 32 characters')
|
|
}
|
|
|
|
// Only flag the refresh secret when it is set explicitly; it falls back to
|
|
// JWT_SECRET when unset (see nuxt.config runtimeConfig).
|
|
if (refreshSecret && looksLikePlaceholder(refreshSecret)) {
|
|
problems.push('JWT_REFRESH_SECRET still uses a placeholder/example value')
|
|
} else if (refreshSecret && refreshSecret.length < 32) {
|
|
problems.push('JWT_REFRESH_SECRET is shorter than 32 characters')
|
|
}
|
|
|
|
if (problems.length === 0) return
|
|
|
|
const detail = problems.map((p) => ` - ${p}`).join('\n')
|
|
const message = `[startup] Insecure auth configuration:\n${detail}`
|
|
|
|
if (isProd) {
|
|
throw new Error(
|
|
`${message}\nRefusing to start. Generate strong secrets, e.g. \`openssl rand -hex 32\`.`,
|
|
)
|
|
}
|
|
|
|
console.warn(`${message}\n(allowed in development — set strong secrets before deploying)`)
|
|
})
|