diff --git a/src/security/hardening.test.ts b/src/security/hardening.test.ts index 4306addf3..d30f3553d 100644 --- a/src/security/hardening.test.ts +++ b/src/security/hardening.test.ts @@ -31,7 +31,12 @@ import { isFsMonitorActive, isSensitivePath, } from "./fs-monitor.js"; -import { initHardening, isHardeningEnabled, teardownHardening } from "./hardening.js"; +import { + HardeningInitError, + initHardening, + isHardeningEnabled, + teardownHardening, +} from "./hardening.js"; // --------------------------------------------------------------------------- // Hardening Logger @@ -553,3 +558,106 @@ describe("hardening integration", () => { expect(result.fsMonitor).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// Fail-Safe Behavior Tests +// --------------------------------------------------------------------------- + +describe("hardening fail-safe behavior", () => { + beforeEach(() => { + __resetSingleUserEnforcerForTest(); + __resetNetworkMonitorForTest(); + __resetFsMonitorForTest(); + __resetHardeningLoggerForTest(); + delete process.env.MOLTBOT_HARDENING_ENABLED; + delete process.env.CLAWDBOT_HARDENING_ENABLED; + delete process.env.MOLTBOT_AUTHORIZED_USER_HASH; + delete process.env.CLAWDBOT_AUTHORIZED_USER_HASH; + }); + + afterEach(() => { + teardownHardening(); + delete process.env.MOLTBOT_HARDENING_ENABLED; + delete process.env.CLAWDBOT_HARDENING_ENABLED; + delete process.env.MOLTBOT_AUTHORIZED_USER_HASH; + delete process.env.CLAWDBOT_AUTHORIZED_USER_HASH; + }); + + it("throws HardeningInitError when single-user enforcer fails with invalid hash", () => { + process.env.MOLTBOT_HARDENING_ENABLED = "1"; + process.env.MOLTBOT_AUTHORIZED_USER_HASH = "invalid-not-a-valid-sha256-hash"; + + expect(() => initHardening({})).toThrow(HardeningInitError); + + try { + initHardening({}); + } catch (err) { + expect(err).toBeInstanceOf(HardeningInitError); + const hardeningErr = err as HardeningInitError; + expect(hardeningErr.failedModules).toContain("single-user-enforcer"); + expect(hardeningErr.message).toContain("MOLTBOT_HARDENING_ENABLED=1"); + expect(hardeningErr.message).toContain("insecure state"); + } + }); + + it("does not throw when hardening is disabled even with invalid config", () => { + // Hardening disabled - should return inactive without throwing + process.env.MOLTBOT_AUTHORIZED_USER_HASH = "invalid-hash"; + + const result = initHardening({}); + expect(result.active).toBe(false); + }); + + it("HardeningInitError has correct name and properties", () => { + process.env.MOLTBOT_HARDENING_ENABLED = "1"; + process.env.MOLTBOT_AUTHORIZED_USER_HASH = "bad"; + + try { + initHardening({}); + expect.fail("Should have thrown HardeningInitError"); + } catch (err) { + expect(err).toBeInstanceOf(HardeningInitError); + const hardeningErr = err as HardeningInitError; + expect(hardeningErr.name).toBe("HardeningInitError"); + expect(Array.isArray(hardeningErr.failedModules)).toBe(true); + expect(hardeningErr.failedModules.length).toBeGreaterThan(0); + } + }); + + it("error message includes all failed module names", () => { + process.env.MOLTBOT_HARDENING_ENABLED = "1"; + process.env.MOLTBOT_AUTHORIZED_USER_HASH = "not-valid"; + + try { + initHardening({}); + expect.fail("Should have thrown"); + } catch (err) { + const hardeningErr = err as HardeningInitError; + expect(hardeningErr.message).toContain("single-user-enforcer"); + } + }); + + it("succeeds when all modules initialize correctly", () => { + const validHash = crypto.createHash("sha256").update("+1234567890").digest("hex"); + process.env.MOLTBOT_HARDENING_ENABLED = "1"; + process.env.MOLTBOT_AUTHORIZED_USER_HASH = validHash; + + // Should not throw + const result = initHardening({}); + expect(result.active).toBe(true); + expect(result.singleUser).toBe(true); + expect(result.networkMonitor).toBe(true); + expect(result.fsMonitor).toBe(true); + }); + + it("succeeds without single-user if no hash is provided (not a failure)", () => { + process.env.MOLTBOT_HARDENING_ENABLED = "1"; + // No hash provided - single-user is simply not enabled, not a failure + + const result = initHardening({}); + expect(result.active).toBe(true); + expect(result.singleUser).toBe(false); + expect(result.networkMonitor).toBe(true); + expect(result.fsMonitor).toBe(true); + }); +}); diff --git a/src/security/hardening.ts b/src/security/hardening.ts index 5528f55e8..6edb93327 100644 --- a/src/security/hardening.ts +++ b/src/security/hardening.ts @@ -84,9 +84,28 @@ export function isHardeningEnabled(cfg: MoltbotConfig): boolean { return envFlag === "1" || envFlag === "true"; } +/** + * Custom error class for security hardening initialization failures. + * Thrown when MOLTBOT_HARDENING_ENABLED=1 but modules fail to initialize (fail-safe). + */ +export class HardeningInitError extends Error { + constructor( + message: string, + public readonly failedModules: string[], + ) { + super(message); + this.name = "HardeningInitError"; + } +} + /** * Initialize all security hardening modules. * Should be called very early in gateway startup, before any I/O. + * + * FAIL-SAFE BEHAVIOR: When hardening is explicitly enabled (MOLTBOT_HARDENING_ENABLED=1 + * or config.security.hardening.enabled=true), any initialization failure will throw + * a HardeningInitError. The system must not start in an insecure state when security + * is explicitly requested. */ export function initHardening(cfg: MoltbotConfig): { active: boolean; @@ -106,9 +125,20 @@ export function initHardening(cfg: MoltbotConfig): { } const config = resolveHardeningConfig(cfg); + const failedModules: string[] = []; + const moduleErrors: Record = {}; // 1. Initialize audit logger first (all other modules depend on it). - initHardeningLogger(); + try { + initHardeningLogger(); + } catch (err) { + // Logger failure is critical - we can't even log what went wrong + throw new HardeningInitError( + `[hardening] FATAL: Audit logger failed to initialize: ${String(err)}. ` + + `Refusing to start with MOLTBOT_HARDENING_ENABLED=1 in insecure state.`, + ["hardening-logger"], + ); + } logSecurityEvent("hardening_init", { module: "hardening", @@ -127,6 +157,8 @@ export function initHardening(cfg: MoltbotConfig): { initSingleUserEnforcer({ authorizedUserHash: userHash }); singleUserActive = true; } catch (err) { + failedModules.push("single-user-enforcer"); + moduleErrors["single-user-enforcer"] = String(err); logSecurityEvent("hardening_error", { module: "single-user-enforcer", error: String(err), @@ -149,6 +181,8 @@ export function initHardening(cfg: MoltbotConfig): { }); networkActive = true; } catch (err) { + failedModules.push("network-monitor"); + moduleErrors["network-monitor"] = String(err); logSecurityEvent("hardening_error", { module: "network-monitor", error: String(err), @@ -166,12 +200,31 @@ export function initHardening(cfg: MoltbotConfig): { }); fsActive = true; } catch (err) { + failedModules.push("fs-monitor"); + moduleErrors["fs-monitor"] = String(err); logSecurityEvent("hardening_error", { module: "fs-monitor", error: String(err), }); } + // FAIL-SAFE: If any module failed to initialize when hardening is enabled, refuse to start + if (failedModules.length > 0) { + logSecurityEvent("hardening_error", { + module: "hardening", + message: "FATAL: Security hardening initialization failed", + failedModules, + errors: moduleErrors, + }); + closeHardeningLogger(); + throw new HardeningInitError( + `[hardening] FATAL: Failed to initialize security modules: ${failedModules.join(", ")}. ` + + `Refusing to start with MOLTBOT_HARDENING_ENABLED=1 in insecure state. ` + + `Errors: ${JSON.stringify(moduleErrors)}`, + failedModules, + ); + } + logSecurityEvent("hardening_init", { module: "hardening", message: "Security hardening initialized",