feat(security): add fail-safe behavior for hardening initialization
When MOLTBOT_HARDENING_ENABLED=1 is set but security modules fail to initialize, the system now throws HardeningInitError and refuses to start. This ensures the gateway never runs in an insecure state when security is explicitly requested. - Add HardeningInitError class with failedModules property - Throw on any module initialization failure when hardening is enabled - Log failed modules before throwing for debugging - Add 6 tests for fail-safe behavior https://claude.ai/code/session_01L2vY1VwmwN9x8WTT7cZdi4
This commit is contained in:
parent
4325cac7e4
commit
83bdbed9be
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<string, string> = {};
|
||||
|
||||
// 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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user