Skip to content

Commit bb7df53

Browse files
ralyodioclaude
andcommitted
fix(install-service): prep system dirs with adm group + setgid so module installs and config edits work without sudo
After `sudo threatcrush install-service`, the runtime dirs ended up root:root mode 755, so a non-root `threatcrush modules install` failed with EACCES when trying to git-clone into /etc/threatcrush/modules. Now install-service chgrp's the dirs to `adm` and sets group-writable + setgid on /etc/threatcrush/modules, matching the same permission boundary used by the IPC socket: anyone who can already read system logs can also install modules and edit conf.d. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 176c983 commit bb7df53

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

apps/cli/src/commands/service.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execSync } from 'node:child_process';
2-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
33
import { join } from 'node:path';
44
import chalk from 'chalk';
55
import { banner } from '../core/logger.js';
@@ -46,6 +46,8 @@ export async function installServiceCommand(): Promise<void> {
4646
writeFileSync(UNIT_PATH, unit, { mode: 0o644 });
4747
console.log(chalk.green(` ✓ Installed unit file: ${UNIT_PATH}`));
4848

49+
ensureSystemDirs();
50+
4951
try {
5052
execSync('systemctl daemon-reload', { stdio: 'inherit' });
5153
execSync('systemctl enable threatcrushd.service', { stdio: 'inherit' });
@@ -57,6 +59,39 @@ export async function installServiceCommand(): Promise<void> {
5759
}
5860
}
5961

62+
// systemd `ReadWritePaths=` requires these to exist before the unit starts,
63+
// and we want module installs / config edits to be writable by `adm` group
64+
// members (same boundary used for log read access and IPC socket access).
65+
function ensureSystemDirs(): void {
66+
const dirs: Array<{ path: string; sticky?: boolean }> = [
67+
{ path: '/etc/threatcrush' },
68+
{ path: '/etc/threatcrush/modules', sticky: true },
69+
{ path: '/etc/threatcrush/threatcrushd.conf.d' },
70+
{ path: '/var/log/threatcrush' },
71+
{ path: '/var/lib/threatcrush' },
72+
{ path: '/var/run/threatcrush' },
73+
];
74+
let admGid: number | null = null;
75+
try {
76+
admGid = statSync('/var/log/auth.log').gid;
77+
} catch {
78+
// adm group not present — leave permissions as root-only
79+
}
80+
81+
for (const { path, sticky } of dirs) {
82+
try { mkdirSync(path, { recursive: true }); } catch {}
83+
if (admGid !== null) {
84+
try {
85+
// 2775 = setgid + group writable; setgid makes new files inherit `adm`.
86+
// 0775 for the non-module dirs.
87+
chmodSync(path, sticky ? 0o2775 : 0o775);
88+
execSync(`chgrp adm ${path}`, { stdio: 'ignore' });
89+
} catch { /* best-effort */ }
90+
}
91+
}
92+
console.log(chalk.green(' ✓ Runtime dirs prepared (group `adm` may install modules / edit config without sudo).'));
93+
}
94+
6095
export async function uninstallServiceCommand(): Promise<void> {
6196
banner();
6297

0 commit comments

Comments
 (0)