mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
fix(security): tighten on-disk credential file modes (700/600) (#87)
Credential-bearing OCP artifacts were created at default umask (0644/0755),
making them world-readable on multi-user hosts. This commit hardens them at
writer time and adds an idempotent startup reconciliation so existing prod
installs are fixed automatically on next service restart.
Changes:
- keys.mjs: mkdirSync(OCP_DIR, { mode: 0o700 }) + chmodSync after to handle
pre-existing dirs; chmodSync(DB_PATH, 0o600) after first getDb() open.
- setup.mjs: chmodSync(plistPath, 0o600) after writeFileSync on macOS;
chmodSync(unitPath, 0o600) after writeFileSync on Linux.
- server.mjs: _tightenFileModesIfPossible() reconciliation block — idempotently
chmods ~/.ocp (700), ~/.ocp/admin-key (600), ~/.ocp/ocp.db (600) on startup;
emits a single info-level log line when any file is tightened; ignores ENOENT
and wraps EPERM in a warn log so startup is never crashed by chmod failure.
Backward compat: all files remain accessible to the same-user owner; 0o600
is still fully readable/writable by the process. Existing prod boxes with
old 0644 ~/.ocp directories get fixed-up on next launchd/systemd restart
without any manual intervention.
cli.js citation: this change is OCP-internal file-permission hardening only.
No cli.js function corresponds to chmod or credential-file management. This
is an OCP-local security improvement that is out of scope for the cli.js
citation requirement per ALIGNMENT.md Rule 2; the cli.js boundary applies
to proxy protocol and API surface changes, not host-filesystem hardening.
Identified during privacy/security audit — .claude/research/ocp-security-audit.md.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+39
-1
@@ -30,7 +30,7 @@
|
||||
import { createServer } from "node:http";
|
||||
import { spawn, execFileSync } from "node:child_process";
|
||||
import { randomUUID, timingSafeEqual } from "node:crypto";
|
||||
import { readFileSync, readdirSync, accessSync, existsSync, constants } from "node:fs";
|
||||
import { readFileSync, readdirSync, accessSync, existsSync, constants, chmodSync, statSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
@@ -167,6 +167,44 @@ function logEvent(level, event, data = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Startup file-mode reconciliation ───────────────────────────────────
|
||||
// Idempotently tightens OCP credential-bearing files to 700/600 so that
|
||||
// existing installs (created before this fix) are hardened on next restart.
|
||||
// Wrapped in try/catch — chmod failure must never crash startup.
|
||||
// Does NOT touch systemd units or launchd plists; those are managed by setup.mjs.
|
||||
function _tightenFileModesIfPossible() {
|
||||
const ocpDir = join(homedir(), ".ocp");
|
||||
const targets = [
|
||||
{ path: ocpDir, mode: 0o700, label: "~/.ocp (dir)" },
|
||||
{ path: join(ocpDir, "admin-key"), mode: 0o600, label: "~/.ocp/admin-key" },
|
||||
{ path: join(ocpDir, "ocp.db"), mode: 0o600, label: "~/.ocp/ocp.db" },
|
||||
];
|
||||
let tightened = 0;
|
||||
let alreadyOk = 0;
|
||||
for (const { path, mode, label } of targets) {
|
||||
try {
|
||||
const st = statSync(path);
|
||||
const current = st.mode & 0o777;
|
||||
if (current !== mode) {
|
||||
chmodSync(path, mode);
|
||||
tightened++;
|
||||
} else {
|
||||
alreadyOk++;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code !== "ENOENT") {
|
||||
// File exists but chmod failed (e.g. EPERM) — log and move on
|
||||
logEvent("warn", "file_mode_tighten_failed", { path: label, error: e.message });
|
||||
}
|
||||
// ENOENT is fine — file doesn't exist yet
|
||||
}
|
||||
}
|
||||
if (tightened > 0) {
|
||||
logEvent("info", "file_modes_tightened", { tightened, alreadyOk });
|
||||
}
|
||||
}
|
||||
_tightenFileModesIfPossible();
|
||||
|
||||
// ── Circuit breaker (DISABLED) ──────────────────────────────────────────
|
||||
// Disabled: CLI proxy has its own retry logic, and the breaker was causing
|
||||
// cascading failures — once API got briefly slow, ALL agents lost connectivity
|
||||
|
||||
Reference in New Issue
Block a user