Harden production auth startup and dev-mode access
This commit is contained in:
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
57
scripts/patch-indieauth-devmode-guard.mjs
Normal file
57
scripts/patch-indieauth-devmode-guard.mjs
Normal 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)`);
|
||||
}
|
||||
66
scripts/preflight-production-security.mjs
Normal file
66
scripts/preflight-production-security.mjs
Normal 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");
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user