Skip to content

Commit bb48d4b

Browse files
adversa-aiclaude
andcommitted
fix: stop writing config keys that OpenClaw's schema rejects
The hardener was writing exec.approvals, exec.autoApprove, sandbox.mode, and gateway.mdns.mode to openclaw.json. These keys are not recognized by OpenClaw's runtime config schema, causing "Invalid config" errors that prevent the plugin from loading. Now the hardener only writes valid keys (tools.exec.host, session.dmScope, logging.redactSensitive, gateway.*) and strips any invalid keys already present. The affected audit findings (SC-EXEC-001, SC-EXEC-003, SC-GW-007) are marked as non-auto-fixable with manual remediation instructions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5f69f90 commit bb48d4b

File tree

7 files changed

+124
-83
lines changed

7 files changed

+124
-83
lines changed

secureclaw/src/auditor.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ export async function auditGateway(ctx: AuditContext, deep = false): Promise<Aud
338338
title: 'mDNS broadcasting in full mode',
339339
description: 'mDNS is broadcasting sensitive instance information on the local network.',
340340
evidence: `gateway.mdns.mode = "${gw.mdns.mode}"`,
341-
remediation: 'Set gateway.mdns.mode to "minimal"',
342-
autoFixable: true,
341+
remediation: 'Manually set gateway.mdns.mode to "minimal" (not auto-fixable — key not in OpenClaw config schema)',
342+
autoFixable: false,
343343
references: [],
344344
owaspAsi: 'ASI05',
345345
});
@@ -640,8 +640,8 @@ export async function auditExecution(ctx: AuditContext): Promise<AuditFinding[]>
640640
title: 'Execution approvals disabled',
641641
description: 'exec.approvals is set to "off". The agent can execute arbitrary commands without user confirmation.',
642642
evidence: 'exec.approvals = "off"',
643-
remediation: 'Set exec.approvals to "always" in openclaw.json',
644-
autoFixable: true,
643+
remediation: 'Manually set exec.approvals to "always" in your OpenClaw settings (not auto-fixable — key not in OpenClaw config schema)',
644+
autoFixable: false,
645645
references: ['CVE-2026-25253'],
646646
owaspAsi: 'ASI02',
647647
});
@@ -672,8 +672,8 @@ export async function auditExecution(ctx: AuditContext): Promise<AuditFinding[]>
672672
title: 'Sandbox mode not set to "all"',
673673
description: `Sandbox mode is "${ctx.config.sandbox?.mode ?? 'undefined'}". Not all commands run in a sandboxed environment.`,
674674
evidence: `sandbox.mode = "${ctx.config.sandbox?.mode ?? 'undefined'}"`,
675-
remediation: 'Set sandbox.mode to "all" in openclaw.json',
676-
autoFixable: true,
675+
remediation: 'Manually set sandbox.mode to "all" in your OpenClaw settings (not auto-fixable — key not in OpenClaw config schema)',
676+
autoFixable: false,
677677
references: [],
678678
owaspAsi: 'ASI05',
679679
});

secureclaw/src/hardener.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,13 @@ describe('hardener', () => {
7373
const result = await harden({ full: true, context: ctx });
7474
expect(result.results.length).toBeGreaterThan(0);
7575

76-
// Check that config was updated
76+
// Check that config was updated with valid OpenClaw keys
7777
const updated = JSON.parse(await fs.readFile(configPath, 'utf-8'));
7878
expect(updated.gateway.bind).toBe('loopback');
79-
expect(updated.exec.approvals).toBe('always');
79+
expect(updated.tools.exec.host).toBe('sandbox');
80+
// exec and sandbox keys should be stripped (not valid in OpenClaw schema)
81+
expect(updated.exec).toBeUndefined();
82+
expect(updated.sandbox).toBeUndefined();
8083
});
8184

8285
it('writes a manifest file', async () => {

secureclaw/src/hardening/config-hardening.test.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,28 @@ describe('config-hardening', () => {
4141
};
4242
}
4343

44-
it('sets exec.approvals to always', async () => {
44+
it('does NOT write exec.approvals (not in OpenClaw schema)', async () => {
4545
const configPath = path.join(tmpDir, 'openclaw.json');
4646
await fs.writeFile(configPath, JSON.stringify({ exec: { approvals: 'off' } }), 'utf-8');
4747

4848
const ctx = makeCtx({ exec: { approvals: 'off' } });
4949
await configHardening.fix(ctx, backupDir);
5050

5151
const updated = JSON.parse(await fs.readFile(configPath, 'utf-8'));
52-
expect(updated.exec.approvals).toBe('always');
52+
// exec key should not be created/modified by the hardener
53+
expect(updated.exec).toBeUndefined();
5354
});
5455

55-
it('sets sandbox.mode to all', async () => {
56+
it('does NOT write sandbox.mode (not in OpenClaw schema)', async () => {
5657
const configPath = path.join(tmpDir, 'openclaw.json');
5758
await fs.writeFile(configPath, JSON.stringify({ sandbox: { mode: 'off' } }), 'utf-8');
5859

5960
const ctx = makeCtx({ sandbox: { mode: 'off' } });
6061
await configHardening.fix(ctx, backupDir);
6162

6263
const updated = JSON.parse(await fs.readFile(configPath, 'utf-8'));
63-
expect(updated.sandbox.mode).toBe('all');
64+
// sandbox key should not be created/modified by the hardener
65+
expect(updated.sandbox).toBeUndefined();
6466
});
6567

6668
it('sets tools.exec.host to sandbox', async () => {
@@ -96,25 +98,42 @@ describe('config-hardening', () => {
9698
expect(updated.logging.redactSensitive).toBe('tools');
9799
});
98100

99-
it('clears autoApprove list', async () => {
101+
it('does NOT write exec.autoApprove (not in OpenClaw schema)', async () => {
100102
const configPath = path.join(tmpDir, 'openclaw.json');
101103
await fs.writeFile(configPath, JSON.stringify({ exec: { autoApprove: ['ls', 'cat'] } }), 'utf-8');
102104

103105
const ctx = makeCtx({ exec: { autoApprove: ['ls', 'cat'] } });
104106
await configHardening.fix(ctx, backupDir);
105107

106108
const updated = JSON.parse(await fs.readFile(configPath, 'utf-8'));
107-
expect(updated.exec.autoApprove).toEqual([]);
109+
// exec key should not be created/modified by the hardener
110+
expect(updated.exec).toBeUndefined();
108111
});
109112

110113
it('creates backup before changes', async () => {
111114
const configPath = path.join(tmpDir, 'openclaw.json');
112-
await fs.writeFile(configPath, JSON.stringify({ exec: { approvals: 'off' } }), 'utf-8');
115+
await fs.writeFile(configPath, JSON.stringify({ tools: { exec: { host: 'gateway' } } }), 'utf-8');
113116

114-
const ctx = makeCtx({ exec: { approvals: 'off' } });
117+
const ctx = makeCtx({ tools: { exec: { host: 'gateway' } } });
115118
await configHardening.fix(ctx, backupDir);
116119

117120
const backupExists = await fs.access(path.join(backupDir, 'openclaw-config.json')).then(() => true).catch(() => false);
118121
expect(backupExists).toBe(true);
119122
});
123+
124+
it('check() reports exec.approvals=off as non-auto-fixable', async () => {
125+
const ctx = makeCtx({ exec: { approvals: 'off' } });
126+
const findings = await configHardening.check(ctx);
127+
const finding = findings.find(f => f.id === 'SC-EXEC-001');
128+
expect(finding).toBeDefined();
129+
expect(finding!.autoFixable).toBe(false);
130+
});
131+
132+
it('check() reports sandbox.mode as non-auto-fixable', async () => {
133+
const ctx = makeCtx({ sandbox: { mode: 'off' } });
134+
const findings = await configHardening.check(ctx);
135+
const finding = findings.find(f => f.id === 'SC-EXEC-003');
136+
expect(finding).toBeDefined();
137+
expect(finding!.autoFixable).toBe(false);
138+
});
120139
});

secureclaw/src/hardening/config-hardening.ts

Lines changed: 35 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ export const configHardening: HardeningModule = {
3737
severity: 'CRITICAL',
3838
category: 'execution',
3939
title: 'Execution approvals disabled',
40-
description: 'Will set exec.approvals to "always".',
40+
description: 'Execution approvals are disabled. This allows commands to run without user confirmation.',
4141
evidence: 'exec.approvals = "off"',
42-
remediation: 'Set exec.approvals to "always"',
43-
autoFixable: true,
42+
remediation: 'Manually set exec.approvals to "always" in your OpenClaw settings (not auto-fixable — key not in OpenClaw config schema)',
43+
autoFixable: false,
4444
references: [],
4545
owaspAsi: 'ASI02',
4646
});
@@ -52,10 +52,10 @@ export const configHardening: HardeningModule = {
5252
severity: 'MEDIUM',
5353
category: 'execution',
5454
title: 'Sandbox not set to all',
55-
description: 'Will set sandbox.mode to "all".',
55+
description: 'Sandbox mode is not set to "all". Not all commands run in a sandboxed environment.',
5656
evidence: `sandbox.mode = "${ctx.config.sandbox?.mode ?? 'undefined'}"`,
57-
remediation: 'Set sandbox.mode to "all"',
58-
autoFixable: true,
57+
remediation: 'Manually set sandbox.mode to "all" in your OpenClaw settings (not auto-fixable — key not in OpenClaw config schema)',
58+
autoFixable: false,
5959
references: [],
6060
owaspAsi: 'ASI05',
6161
});
@@ -98,32 +98,12 @@ export const configHardening: HardeningModule = {
9898

9999
const config = await readConfig(ctx.stateDir);
100100

101-
// 1. Enable approval mode
102-
if (!config.exec) config.exec = {};
103-
const oldApprovals = config.exec.approvals;
104-
if (oldApprovals !== 'always') {
105-
config.exec.approvals = 'always';
106-
applied.push({
107-
id: 'config-approvals',
108-
description: 'Set exec.approvals to "always"',
109-
before: oldApprovals ?? 'undefined',
110-
after: 'always',
111-
});
112-
}
113-
114-
// 2. Force sandbox execution
115-
if (!config.sandbox) config.sandbox = {};
116-
const oldSandboxMode = config.sandbox.mode;
117-
if (oldSandboxMode !== 'all') {
118-
config.sandbox.mode = 'all';
119-
applied.push({
120-
id: 'config-sandbox-mode',
121-
description: 'Set sandbox.mode to "all"',
122-
before: oldSandboxMode ?? 'undefined',
123-
after: 'all',
124-
});
125-
}
101+
// NOTE: We only write keys that OpenClaw's runtime schema accepts.
102+
// Keys like exec.approvals, exec.autoApprove, sandbox.mode are NOT
103+
// valid in OpenClaw's config and would cause "Invalid config" errors.
104+
// Those settings are reported as audit findings with manual remediation.
126105

106+
// 1. Set tools.exec.host to sandbox (valid OpenClaw key)
127107
if (!config.tools) config.tools = {};
128108
if (!config.tools.exec) config.tools.exec = {};
129109
const oldExecHost = config.tools.exec.host;
@@ -137,23 +117,7 @@ export const configHardening: HardeningModule = {
137117
});
138118
}
139119

140-
// 3. Disable auto-approval
141-
if (!config.exec.autoApprove || config.exec.autoApprove.length > 0) {
142-
config.exec.autoApprove = [];
143-
applied.push({
144-
id: 'config-auto-approve',
145-
description: 'Cleared exec.autoApprove list',
146-
before: 'had auto-approved commands',
147-
after: '[] (nothing auto-approved)',
148-
});
149-
}
150-
151-
// 4. Set DM policy to pairing for open channels
152-
// Note: Channel configs are typically separate but we handle them via the main config
153-
// In a real deployment, each channel has its own config.
154-
// We'll store the fix intent in secureclaw config.
155-
156-
// 5. Enable DM session isolation
120+
// 2. Enable DM session isolation (valid OpenClaw key)
157121
if (!config.session) config.session = {};
158122
const oldDmScope = config.session.dmScope;
159123
if (oldDmScope !== 'per-channel-peer') {
@@ -166,7 +130,7 @@ export const configHardening: HardeningModule = {
166130
});
167131
}
168132

169-
// 6. Enable sensitive log redaction
133+
// 3. Enable sensitive log redaction (valid OpenClaw key)
170134
if (!config.logging) config.logging = {};
171135
const oldRedact = config.logging.redactSensitive;
172136
if (oldRedact !== 'tools') {
@@ -179,6 +143,28 @@ export const configHardening: HardeningModule = {
179143
});
180144
}
181145

146+
// 4. Strip keys that are NOT in OpenClaw's config schema to avoid
147+
// "Invalid config" / "Unrecognized key" errors on startup.
148+
const configAny = config as Record<string, unknown>;
149+
if (configAny['exec']) {
150+
delete configAny['exec'];
151+
applied.push({
152+
id: 'config-strip-exec',
153+
description: 'Removed invalid root-level "exec" key (not in OpenClaw schema)',
154+
before: 'present',
155+
after: 'removed',
156+
});
157+
}
158+
if (configAny['sandbox']) {
159+
delete configAny['sandbox'];
160+
applied.push({
161+
id: 'config-strip-sandbox',
162+
description: 'Removed invalid root-level "sandbox" key (not in OpenClaw schema)',
163+
before: 'present',
164+
after: 'removed',
165+
});
166+
}
167+
182168
await writeConfig(ctx.stateDir, config);
183169
} catch (err) {
184170
errors.push(`Config hardening error: ${err instanceof Error ? err.message : String(err)}`);

secureclaw/src/hardening/gateway-hardening.test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,12 @@ describe('gateway-hardening', () => {
9696
expect(updatedConfig.gateway.controlUi.allowInsecureAuth).toBe(false);
9797
});
9898

99-
it('fix() is idempotent', async () => {
99+
it('fix() is idempotent when config already hardened', async () => {
100100
const configPath = path.join(tmpDir, 'openclaw.json');
101101
await fs.writeFile(configPath, JSON.stringify({
102102
gateway: {
103103
bind: 'loopback',
104104
auth: { mode: 'password', password: 'a'.repeat(64) },
105-
mdns: { mode: 'minimal' },
106105
controlUi: { dangerouslyDisableDeviceAuth: false, allowInsecureAuth: false },
107106
trustedProxies: [],
108107
},
@@ -112,7 +111,6 @@ describe('gateway-hardening', () => {
112111
gateway: {
113112
bind: 'loopback',
114113
auth: { mode: 'password', password: 'a'.repeat(64) },
115-
mdns: { mode: 'minimal' },
116114
controlUi: { dangerouslyDisableDeviceAuth: false, allowInsecureAuth: false },
117115
trustedProxies: [],
118116
},
@@ -123,4 +121,36 @@ describe('gateway-hardening', () => {
123121
const result = await gatewayHardening.fix(ctx, backupDir2);
124122
expect(result.applied).toHaveLength(0);
125123
});
124+
125+
it('fix() strips gateway.mdns key (not in OpenClaw schema)', async () => {
126+
const configPath = path.join(tmpDir, 'openclaw.json');
127+
await fs.writeFile(configPath, JSON.stringify({
128+
gateway: {
129+
bind: 'loopback',
130+
auth: { mode: 'password', password: 'a'.repeat(64) },
131+
mdns: { mode: 'full' },
132+
controlUi: {},
133+
trustedProxies: [],
134+
},
135+
}), 'utf-8');
136+
137+
const ctx = makeCtx({
138+
gateway: {
139+
bind: 'loopback',
140+
auth: { mode: 'password', password: 'a'.repeat(64) },
141+
mdns: { mode: 'full' },
142+
controlUi: {},
143+
trustedProxies: [],
144+
},
145+
});
146+
147+
const backupDir2 = path.join(tmpDir, 'backup2');
148+
await fs.mkdir(backupDir2, { recursive: true });
149+
const result = await gatewayHardening.fix(ctx, backupDir2);
150+
151+
// mdns key should be stripped
152+
const updated = JSON.parse(await fs.readFile(configPath, 'utf-8'));
153+
expect(updated.gateway.mdns).toBeUndefined();
154+
expect(result.applied.some(a => a.id === 'gw-strip-mdns')).toBe(true);
155+
});
126156
});

secureclaw/src/hardening/gateway-hardening.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ export const gatewayHardening: HardeningModule = {
116116
severity: 'MEDIUM',
117117
category: 'gateway',
118118
title: 'mDNS in full mode',
119-
description: 'Will set mDNS to minimal mode.',
119+
description: 'mDNS is broadcasting in full mode, exposing service information to the local network.',
120120
evidence: `mdns.mode = "${gw.mdns.mode}"`,
121-
remediation: 'Will set to "minimal"',
122-
autoFixable: true,
121+
remediation: 'Manually set gateway.mdns.mode to "minimal" (not auto-fixable — key not in OpenClaw config schema)',
122+
autoFixable: false,
123123
references: [],
124124
owaspAsi: 'ASI05',
125125
});
@@ -206,16 +206,16 @@ export const gatewayHardening: HardeningModule = {
206206
});
207207
}
208208

209-
// 4. Set mDNS to minimal
210-
if (!config.gateway.mdns) config.gateway.mdns = {};
211-
const oldMdns = config.gateway.mdns.mode;
212-
if (oldMdns !== 'minimal') {
213-
config.gateway.mdns.mode = 'minimal';
209+
// 4. Strip gateway.mdns — NOT a valid OpenClaw config key.
210+
// mDNS findings are reported as non-auto-fixable in the auditor.
211+
if (config.gateway.mdns) {
212+
const gwAny = config.gateway as Record<string, unknown>;
213+
delete gwAny['mdns'];
214214
applied.push({
215-
id: 'gw-mdns',
216-
description: 'Set mDNS to minimal mode',
217-
before: oldMdns ?? 'undefined',
218-
after: 'minimal',
215+
id: 'gw-strip-mdns',
216+
description: 'Removed invalid "gateway.mdns" key (not in OpenClaw schema)',
217+
before: 'present',
218+
after: 'removed',
219219
});
220220
}
221221

secureclaw/src/integration.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,14 @@ describe('SecureClaw Integration', { timeout: 30000 }, () => {
309309
expect(updated.gateway.bind).toBe('loopback');
310310
expect(updated.gateway.auth.mode).toBe('password');
311311
expect(updated.gateway.auth.password.length).toBeGreaterThanOrEqual(64);
312-
expect(updated.exec.approvals).toBe('always');
313-
expect(updated.sandbox.mode).toBe('all');
312+
expect(updated.tools.exec.host).toBe('sandbox');
313+
// exec, sandbox, and gateway.mdns are NOT valid OpenClaw config keys
314+
// The hardener strips them to avoid "Invalid config" errors
315+
expect(updated.exec).toBeUndefined();
316+
expect(updated.sandbox).toBeUndefined();
317+
expect(updated.gateway.mdns).toBeUndefined();
314318
expect(updated.gateway.controlUi.dangerouslyDisableDeviceAuth).toBe(false);
315319
expect(updated.gateway.controlUi.allowInsecureAuth).toBe(false);
316-
expect(updated.gateway.mdns.mode).toBe('minimal');
317320
});
318321

319322
it('creates encrypted .env.enc file', async () => {

0 commit comments

Comments
 (0)