diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d7a909de..f4847226 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,7 +43,7 @@ jobs: # Ensure env file exists and contains auth secrets required by start.sh. sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && test -f .env"' - sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && if ! (grep -Eq \"^SECRET=.*\" .env && grep -Eq \"^PASSWORD_SECRET=.*\" .env); then echo \"Missing SECRET or PASSWORD_SECRET in /usr/local/indiekit/.env\"; exit 1; fi"' + sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && if ! grep -Eq \"^SECRET=.*\" .env; then echo \"Missing SECRET in /usr/local/indiekit/.env\"; exit 1; fi; if ! (grep -Eq \"^PASSWORD_SECRET=.*\" .env || grep -Eq \"^INDIEKIT_ALLOW_PASSWORD_SETUP=1\" .env); then echo \"Missing PASSWORD_SECRET (or set INDIEKIT_ALLOW_PASSWORD_SETUP=1 for one-time recovery) in /usr/local/indiekit/.env\"; exit 1; fi"' # Validate startup prerequisites before touching the running service. sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && NODE_ENV=production node scripts/preflight-production-security.mjs"' diff --git a/README.md b/README.md index 7f17cf9e..ff5f779c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - When `INDIEKIT_ADMIN_URL` is set, config wires absolute auth endpoints/callback base (`/auth`, `/auth/token`, `/auth/introspect`) to that URL to keep login redirects on `/admin/*`. - Login uses `PASSWORD_SECRET` (bcrypt hash), not `INDIEKIT_PASSWORD`. - If no `PASSWORD_SECRET` exists yet, open `/admin/auth/new-password` once to generate it. +- If login is blocked because `PASSWORD_SECRET` is missing/invalid, set `INDIEKIT_ALLOW_PASSWORD_SETUP=1` temporarily, restart, generate a new hash via `/admin/auth/new-password`, set `PASSWORD_SECRET` to that hash, then remove `INDIEKIT_ALLOW_PASSWORD_SETUP`. - If login appears passwordless, first check for an existing authenticated session cookie. Use `/session/logout` (or `/admin/session/logout` behind proxy) to force a fresh login challenge. - Upstream IndieKit auto-authenticates in dev mode (`NODE_ENV=development`). This repository patches that behavior so dev auto-auth only works when `INDIEKIT_ALLOW_DEV_AUTH=1` is explicitly set. - Production startup now fails closed when auth/session settings are unsafe (`NODE_ENV` not `production`, `INDIEKIT_ALLOW_DEV_AUTH=1`, weak `SECRET`, missing/invalid `PASSWORD_SECRET`, or empty-password hash). @@ -73,6 +74,7 @@ - Startup scripts parse `.env` with the `dotenv` parser (not shell `source`), so values containing spaces are handled safely. - Startup scripts run preflight + patch helpers before boot (`scripts/preflight-production-security.mjs`, `scripts/preflight-mongo-connection.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-media-sharp-runtime.mjs`, `scripts/patch-frontend-sharp-runtime.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-endpoint-files-upload-locales.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`, `scripts/patch-indieauth-devmode-guard.mjs`). - The production security preflight blocks startup on insecure auth/session configuration and catches empty-password bcrypt hashes. +- One-time recovery mode is available with `INDIEKIT_ALLOW_PASSWORD_SETUP=1` to bootstrap/reset `PASSWORD_SECRET` when locked out. Remove this flag after setting a valid hash. - The media scope patch fixes a known upstream issue where file uploads can fail if the token scope is `create update delete` without explicit `media`. - The media sharp runtime patch makes image transformation resilient on FreeBSD: if `sharp` cannot load, uploads continue without resize/rotation instead of crashing the server process. - The frontend sharp runtime patch makes icon generation non-fatal on FreeBSD when `sharp` cannot load, preventing startup crashes in asset controller imports. diff --git a/scripts/preflight-production-security.mjs b/scripts/preflight-production-security.mjs index 7a871cf3..fa7bf4ef 100644 --- a/scripts/preflight-production-security.mjs +++ b/scripts/preflight-production-security.mjs @@ -4,6 +4,7 @@ import bcrypt from "bcrypt"; const strictMode = process.env.REQUIRE_SECURITY !== "0"; const nodeEnv = (process.env.NODE_ENV || "").toLowerCase(); +const allowPasswordSetup = process.env.INDIEKIT_ALLOW_PASSWORD_SETUP === "1"; function failOrWarn(message) { if (strictMode) { @@ -35,21 +36,29 @@ if (secret.length < 32) { const passwordSecret = process.env.PASSWORD_SECRET || ""; if (!passwordSecret) { - failOrWarn("[preflight] PASSWORD_SECRET is required."); + if (allowPasswordSetup) { + console.warn( + "[preflight] PASSWORD setup recovery mode enabled. Start app, generate a hash at /auth/new-password, then disable INDIEKIT_ALLOW_PASSWORD_SETUP.", + ); + } else { + failOrWarn("[preflight] PASSWORD_SECRET is required."); + } } -if (!/^\$2[aby]\$\d{2}\$/.test(passwordSecret)) { +if (passwordSecret && !/^\$2[aby]\$\d{2}\$/.test(passwordSecret)) { failOrWarn( "[preflight] PASSWORD_SECRET must be a bcrypt hash (starts with $2a$, $2b$, or $2y$).", ); } try { - const emptyPasswordValid = await bcrypt.compare("", passwordSecret); - if (emptyPasswordValid) { - failOrWarn( - "[preflight] PASSWORD_SECRET matches an empty password. Generate a non-empty password hash via /auth/new-password.", - ); + if (passwordSecret) { + const emptyPasswordValid = await bcrypt.compare("", passwordSecret); + if (emptyPasswordValid) { + failOrWarn( + "[preflight] PASSWORD_SECRET matches an empty password. Generate a non-empty password hash via /auth/new-password.", + ); + } } } catch (error) { failOrWarn( diff --git a/start.example.sh b/start.example.sh index 86080437..6116fc41 100644 --- a/start.example.sh +++ b/start.example.sh @@ -18,7 +18,9 @@ if [ -f .env ]; then fi : "${SECRET:?SECRET is required}" -: "${PASSWORD_SECRET:?PASSWORD_SECRET is required}" +if [ "${INDIEKIT_ALLOW_PASSWORD_SETUP:-0}" != "1" ]; then + : "${PASSWORD_SECRET:?PASSWORD_SECRET is required}" +fi # Allow either full Mongo URL or decomposed credentials. if [ -z "${MONGO_URL:-}" ]; then