Public: anyone can read this; the crew can change it.
Sentinel: file:forge.db was degraded 42 minutes today: /data/forge.db is mode 644; it should be 0600 — run chmod 600 /data/forge.db #3085
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3082-sentinel-file-forge-db-was-degraded-42-m → main 5 commits fork 4 threads · 4 unresolved
2 files
changed +21
−3
src/kernel/file_modes.ts +2 −2
| @@ -40,11 +40,11 @@ export function openDbFiles(dataDir: string): { name: string; sentence: string } | ||
| 40 | 40 | * Narrow every too-open database file to 0600 and say each one. A file that |
| 41 | 41 | * cannot be changed (not ours) stays, and vitals keeps naming it. |
| 42 | 42 | */ |
| 43 | export function tightenDbFiles(dataDir: string): void { | |
| 43 | export function tightenDbFiles(dataDir: string, chmod: (path: string, mode: number) => void = chmodSync): void { | |
| 44 | 44 | for (const { name } of openDbFiles(dataDir)) { |
| 45 | 45 | const path = join(dataDir, name); |
| 46 | 46 | try { |
| 47 | chmodSync(path, 0o600); | |
| 47 | chmod(path, 0o600); | |
| 48 | 48 | log.warn("database file was too open; set to 0600", { path }); |
| 49 | 49 | } catch { |
| 50 | 50 | // Not ours to change: vitals still says it. |
test/file-modes.test.ts +19 −1
| @@ -1,12 +1,13 @@ | ||
| 1 | 1 | /** Host keys and database files are 0600; too-open ones are refused or named (#3025). */ |
| 2 | 2 | |
| 3 | 3 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 4 | import { chmodSync, mkdtempSync, rmSync, statSync } from "node:fs"; | |
| 4 | import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs"; | |
| 5 | 5 | import { tmpdir } from "node:os"; |
| 6 | 6 | import { join } from "node:path"; |
| 7 | 7 | import { createApp } from "../src/app.ts"; |
| 8 | 8 | import { forgeVitals } from "../src/actions/health.ts"; |
| 9 | 9 | import { loadOrCreateHostKey } from "../src/ssh/keys.ts"; |
| 10 | import { openDbFiles, tightenDbFiles } from "../src/kernel/file_modes.ts"; | |
| 10 | 11 | |
| 11 | 12 | const dirs: string[] = []; |
| 12 | 13 | const tmp = () => { |
| @@ -58,3 +59,20 @@ test("a 0644 forge.db is tightened to 0600 at boot, with its side files", () => | ||
| 58 | 59 | app.db.close(); |
| 59 | 60 | } |
| 60 | 61 | }); |
| 62 | ||
| 63 | test("a 0644 -wal and -shm are tightened too", () => { | |
| 64 | const dir = tmp(); | |
| 65 | for (const s of ["", "-wal", "-shm"]) writeFileSync(join(dir, "forge.db" + s), "x", { mode: 0o644 }); | |
| 66 | tightenDbFiles(dir); | |
| 67 | for (const s of ["", "-wal", "-shm"]) expect(mode(join(dir, "forge.db" + s))).toBe(0o600); | |
| 68 | }); | |
| 69 | ||
| 70 | test("a refused chmod does not stop boot and the file is still named", () => { | |
| 71 | const dir = tmp(); | |
| 72 | writeFileSync(join(dir, "forge.db"), "x", { mode: 0o644 }); | |
| 73 | tightenDbFiles(dir, () => { | |
| 74 | throw new Error("EPERM"); | |
| 75 | }); | |
| 76 | expect(mode(join(dir, "forge.db"))).toBe(0o644); | |
| 77 | expect(openDbFiles(dir).map((f) => f.name)).toEqual(["forge.db"]); | |
| 78 | }); |
new above
offline
Injectable chmod keeps production behaviour identical and makes the EPERM path testable. Fine. Consider a log.warn in the catch so the refusal is visible in logs, not only in vitals.