diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c4aedd1..635191f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete - Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete - Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c - Outbound: default Telegram account selection for config-only tokens; remove heartbeat-specific accountId handling. (follow-up #516) — thanks @YuriNachos diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 0b0513850..be9351741 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -59,6 +59,7 @@ cat ~/.clawdbot/clawdbot.json - Legacy config migration and normalization. - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth). - State integrity and permissions checks (sessions, transcripts, state dir). +- Config file permission checks (chmod 600) when running locally. - Legacy workspace dir detection (`~/clawdis`, `~/clawdbot`). - Sandbox image repair when sandboxing is enabled. - Legacy service migration and extra gateway detection. @@ -129,6 +130,8 @@ Doctor checks: split between installs). - **Remote mode reminder**: if `gateway.mode=remote`, doctor reminds you to run it on the remote host (the state lives there). +- **Config file permissions**: warns if `~/.clawdbot/clawdbot.json` is + group/world readable and offers to tighten to `600`. ### 5) Sandbox image repair When sandboxing is enabled, doctor checks Docker images and offers to build or diff --git a/docs/gateway/security.md b/docs/gateway/security.md index e60206734..3dda917b7 100644 --- a/docs/gateway/security.md +++ b/docs/gateway/security.md @@ -95,6 +95,14 @@ This is social engineering 101. Create distrust, encourage snooping. ## Configuration Hardening (examples) +### 0) File permissions + +Keep config + state private on the gateway host: +- `~/.clawdbot/clawdbot.json`: `600` (user read/write only) +- `~/.clawdbot`: `700` (user only) + +`clawdbot doctor` can warn and offer to tighten these permissions. + ### 1) DMs: pairing by default ```json5 diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index 49ad12520..195b6f520 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -123,6 +123,7 @@ function findOtherStateDirs(stateDir: string): string[] { export async function noteStateIntegrity( cfg: ClawdbotConfig, prompter: DoctorPrompterLike, + configPath?: string, ) { const warnings: string[] = []; const changes: string[] = []; @@ -186,6 +187,49 @@ export async function noteStateIntegrity( } } } + if (stateDirExists && process.platform !== "win32") { + try { + const stat = fs.statSync(stateDir); + if ((stat.mode & 0o077) !== 0) { + warnings.push( + `- State directory permissions are too open (${stateDir}). Recommend chmod 700.`, + ); + const tighten = await prompter.confirmSkipInNonInteractive({ + message: `Tighten permissions on ${stateDir} to 700?`, + initialValue: true, + }); + if (tighten) { + fs.chmodSync(stateDir, 0o700); + changes.push(`- Tightened permissions on ${stateDir} to 700`); + } + } + } catch (err) { + warnings.push(`- Failed to read ${stateDir} permissions: ${String(err)}`); + } + } + + if (configPath && existsFile(configPath) && process.platform !== "win32") { + try { + const stat = fs.statSync(configPath); + if ((stat.mode & 0o077) !== 0) { + warnings.push( + `- Config file is group/world readable (${configPath}). Recommend chmod 600.`, + ); + const tighten = await prompter.confirmSkipInNonInteractive({ + message: `Tighten permissions on ${configPath} to 600?`, + initialValue: true, + }); + if (tighten) { + fs.chmodSync(configPath, 0o600); + changes.push(`- Tightened permissions on ${configPath} to 600`); + } + } + } catch (err) { + warnings.push( + `- Failed to read config permissions (${configPath}): ${String(err)}`, + ); + } + } if (stateDirExists) { const dirCandidates = new Map(); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index d999d41e4..cb1e6c8b0 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -146,7 +146,7 @@ export async function doctorCommand( } } - await noteStateIntegrity(cfg, prompter); + await noteStateIntegrity(cfg, prompter, snapshot.path ?? CONFIG_PATH_CLAWDBOT); cfg = await maybeRepairSandboxImages(cfg, runtime, prompter); noteSandboxScopeWarnings(cfg);