feat(doctor): audit config + state permissions

This commit is contained in:
Peter Steinberger
2026-01-08 21:51:34 +01:00
parent 13ddd40a59
commit 884e734809
5 changed files with 57 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
## Unreleased ## 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 - 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 - 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 - Outbound: default Telegram account selection for config-only tokens; remove heartbeat-specific accountId handling. (follow-up #516) — thanks @YuriNachos

View File

@@ -59,6 +59,7 @@ cat ~/.clawdbot/clawdbot.json
- Legacy config migration and normalization. - Legacy config migration and normalization.
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth). - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
- State integrity and permissions checks (sessions, transcripts, state dir). - State integrity and permissions checks (sessions, transcripts, state dir).
- Config file permission checks (chmod 600) when running locally.
- Legacy workspace dir detection (`~/clawdis`, `~/clawdbot`). - Legacy workspace dir detection (`~/clawdis`, `~/clawdbot`).
- Sandbox image repair when sandboxing is enabled. - Sandbox image repair when sandboxing is enabled.
- Legacy service migration and extra gateway detection. - Legacy service migration and extra gateway detection.
@@ -129,6 +130,8 @@ Doctor checks:
split between installs). split between installs).
- **Remote mode reminder**: if `gateway.mode=remote`, doctor reminds you to run - **Remote mode reminder**: if `gateway.mode=remote`, doctor reminds you to run
it on the remote host (the state lives there). 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 ### 5) Sandbox image repair
When sandboxing is enabled, doctor checks Docker images and offers to build or When sandboxing is enabled, doctor checks Docker images and offers to build or

View File

@@ -95,6 +95,14 @@ This is social engineering 101. Create distrust, encourage snooping.
## Configuration Hardening (examples) ## 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 ### 1) DMs: pairing by default
```json5 ```json5

View File

@@ -123,6 +123,7 @@ function findOtherStateDirs(stateDir: string): string[] {
export async function noteStateIntegrity( export async function noteStateIntegrity(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
prompter: DoctorPrompterLike, prompter: DoctorPrompterLike,
configPath?: string,
) { ) {
const warnings: string[] = []; const warnings: string[] = [];
const changes: 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) { if (stateDirExists) {
const dirCandidates = new Map<string, string>(); const dirCandidates = new Map<string, string>();

View File

@@ -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); cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
noteSandboxScopeWarnings(cfg); noteSandboxScopeWarnings(cfg);