Harden production auth startup and dev-mode access

This commit is contained in:
svemagie
2026-03-08 04:01:41 +01:00
parent b72b23ed1c
commit 1558e8b40e
6 changed files with 138 additions and 4 deletions

View File

@@ -46,6 +46,7 @@ jobs:
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"'
# 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"'
sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && NODE_ENV=production node scripts/preflight-mongo-connection.mjs"'
# Restart asynchronously to avoid hanging SSH sessions when rc scripts keep stdout open.
@@ -67,6 +68,7 @@ jobs:
echo "Indiekit process not found after restart."
sudo bastille cmd node sh -lc "tail -n 120 ${restart_log} || true"
sudo bastille cmd node sh -lc 'service indiekit onestatus || true'
sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && NODE_ENV=production node scripts/preflight-production-security.mjs" || true'
sudo bastille cmd node sh -lc 'su -l indiekit -c "cd /usr/local/indiekit && NODE_ENV=production node scripts/preflight-mongo-connection.mjs" || true'
exit 1
# Optionally reload nginx on web jail

View File

@@ -7,6 +7,9 @@
- 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 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).
- Post management UI should use `/posts` (`@indiekit/endpoint-posts.mountPath`).
- Do not set post-management `mountPath` to frontend routes like `/blog`, otherwise backend publishing can be shadowed by the public site.
@@ -68,7 +71,8 @@
- `start.sh` is intentionally ignored by Git (`.gitignore`) so server secrets are not committed.
- Use `start.example.sh` as the tracked template and keep real credentials in environment variables (or `.env` on the server).
- 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-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`).
- 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.
- 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.
@@ -76,3 +80,4 @@
- The files upload locale patch adds missing `files.upload.dropText`/`files.upload.browse`/`files.upload.submitMultiple` labels in endpoint locale files so UI text does not render raw translation keys.
- The frontend serviceworker patch ensures `@indiekit/frontend/lib/serviceworker.js` exists at runtime to avoid ENOENT in the offline/service worker route.
- The conversations guard patch prevents `Cannot read properties of undefined (reading 'find')` when the `conversation_items` collection is temporarily unavailable.
- The indieauth dev-mode guard patch prevents accidental production auth bypass by requiring explicit `INDIEKIT_ALLOW_DEV_AUTH=1` to enable dev auto-login.

View File

@@ -4,8 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs",
"serve": "node scripts/preflight-mongo-connection.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indieauth-devmode-guard.mjs",
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],

View File

@@ -0,0 +1,57 @@
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@indiekit/indiekit/lib/indieauth.js",
];
const oldCode = `if (devMode) {
request.session.access_token = process.env.NODE_ENV;
request.session.scope = "create update delete media";
} else if (!process.env.PASSWORD_SECRET) {`;
const newCode = `if (devMode && process.env.INDIEKIT_ALLOW_DEV_AUTH === "1") {
request.session.access_token = process.env.NODE_ENV;
request.session.scope = "create update delete media";
} else if (!process.env.PASSWORD_SECRET) {`;
async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
let checked = 0;
let patched = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) {
continue;
}
checked += 1;
const source = await readFile(filePath, "utf8");
if (source.includes(newCode)) {
continue;
}
if (!source.includes(oldCode)) {
continue;
}
const updated = source.replace(oldCode, newCode);
await writeFile(filePath, updated, "utf8");
patched += 1;
}
if (checked === 0) {
console.log("[postinstall] No indieauth middleware files found");
} else if (patched === 0) {
console.log("[postinstall] indieauth dev-mode guard already patched");
} else {
console.log(`[postinstall] Patched indieauth dev-mode guard in ${patched} file(s)`);
}

View File

@@ -0,0 +1,66 @@
import "dotenv/config";
import bcrypt from "bcrypt";
const strictMode = process.env.REQUIRE_SECURITY !== "0";
const nodeEnv = (process.env.NODE_ENV || "").toLowerCase();
function failOrWarn(message) {
if (strictMode) {
console.error(message);
process.exit(1);
}
console.warn(`${message} Continuing because strict mode is disabled.`);
}
if (nodeEnv !== "production") {
failOrWarn(
`[preflight] NODE_ENV must be "production" for secure startup (received "${process.env.NODE_ENV || "(unset)"}").`,
);
}
if (process.env.INDIEKIT_ALLOW_DEV_AUTH === "1") {
failOrWarn(
"[preflight] INDIEKIT_ALLOW_DEV_AUTH=1 is not allowed in production.",
);
}
const secret = process.env.SECRET || "";
if (secret.length < 32) {
failOrWarn(
"[preflight] SECRET must be set and at least 32 characters long.",
);
}
const passwordSecret = process.env.PASSWORD_SECRET || "";
if (!passwordSecret) {
failOrWarn("[preflight] PASSWORD_SECRET is required.");
}
if (!/^\$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.",
);
}
} catch (error) {
failOrWarn(
`[preflight] PASSWORD_SECRET could not be validated with bcrypt: ${error.message}`,
);
}
if (process.env.INDIEKIT_PASSWORD) {
console.warn(
"[preflight] INDIEKIT_PASSWORD is set but ignored by core auth. Use PASSWORD_SECRET only.",
);
}
console.log("[preflight] Production auth configuration OK");

View File

@@ -37,6 +37,9 @@ export NODE_ENV="production"
export INDIEKIT_DEBUG="0"
unset DEBUG
# Verify production auth/session hardening before launching server.
/usr/local/bin/node scripts/preflight-production-security.mjs
# Verify MongoDB credentials/connectivity before launching server.
/usr/local/bin/node scripts/preflight-mongo-connection.mjs
@@ -49,5 +52,6 @@ unset DEBUG
/usr/local/bin/node scripts/patch-endpoint-files-upload-locales.mjs
/usr/local/bin/node scripts/patch-frontend-serviceworker-file.mjs
/usr/local/bin/node scripts/patch-conversations-collection-guards.mjs
/usr/local/bin/node scripts/patch-indieauth-devmode-guard.mjs
exec /usr/local/bin/node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs