Add safe password setup recovery mode

This commit is contained in:
svemagie
2026-03-08 04:05:06 +01:00
parent 1558e8b40e
commit 8e759a5cb9
4 changed files with 22 additions and 9 deletions

View File

@@ -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"'

View File

@@ -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.

View File

@@ -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(

View File

@@ -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