Route jail exec through a new hostd bastille-cmd operation, remove the agent-jail sudoers requirement, and fall back to repository ownership when elevation does not provide SUDO_* metadata. --- Build: pass | Tests: n/a (Vitest not run in this Linux environment per repo policy) --- Build: pass | Tests: pass — 2375 passed (704 files)
220 lines
6 KiB
TypeScript
220 lines
6 KiB
TypeScript
/**
|
|
* setup/hostd.ts — Install and start the privileged host daemon (hostd).
|
|
*
|
|
* This is a root-only step. It installs an rc.d service that runs:
|
|
* node dist/hostd/index.js
|
|
*
|
|
* The daemon socket path is derived from the platform service identity in
|
|
* src/hostd/types.ts.
|
|
*/
|
|
import { SERVICE_NAME } from '../src/platform-identity.js';
|
|
import { execFileSync, execSync } from 'child_process';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
PLATFORM_RUNTIME_HOME,
|
|
} from '../src/config.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { commandExists, getNodePath, getPlatform, isRoot } from './platform.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
const LOG = 'logs/setup.log';
|
|
|
|
function lookupUserByUid(uid: number): string | null {
|
|
try {
|
|
const user = execFileSync('id', ['-nu', String(uid)], { encoding: 'utf-8' }).trim();
|
|
return user || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getInvokingContext(
|
|
fallbackPath: string,
|
|
): { user: string | null; uid: number | null; gid: number | null } {
|
|
const user = (process.env.SUDO_USER || '').trim() || null;
|
|
const uidRaw = (process.env.SUDO_UID || '').trim();
|
|
const gidRaw = (process.env.SUDO_GID || '').trim();
|
|
const uid = uidRaw ? Number(uidRaw) : null;
|
|
const gid = gidRaw ? Number(gidRaw) : null;
|
|
if (user && Number.isFinite(uid) && Number.isFinite(gid)) {
|
|
return {
|
|
user,
|
|
uid,
|
|
gid,
|
|
};
|
|
}
|
|
try {
|
|
const stat = fs.statSync(fallbackPath);
|
|
return {
|
|
user: stat.uid === 0 ? null : lookupUserByUid(stat.uid),
|
|
uid: stat.uid,
|
|
gid: stat.gid,
|
|
};
|
|
} catch {
|
|
// fall through
|
|
}
|
|
return {
|
|
user: null,
|
|
uid: null,
|
|
gid: null,
|
|
};
|
|
}
|
|
|
|
function buildProject(projectRoot: string): void {
|
|
const invoking = getInvokingContext(projectRoot);
|
|
if (isRoot() && invoking.user && invoking.uid !== 0) {
|
|
execFileSync(
|
|
'su',
|
|
[
|
|
'-m',
|
|
invoking.user,
|
|
'-c',
|
|
`cd ${JSON.stringify(projectRoot)} && npm run build`,
|
|
],
|
|
{ cwd: projectRoot, stdio: 'ignore' },
|
|
);
|
|
return;
|
|
}
|
|
execSync('npm run build', { cwd: projectRoot, stdio: 'ignore' });
|
|
}
|
|
|
|
function chownIfInvokingContext(filePath: string, fallbackPath: string): void {
|
|
const invoking = getInvokingContext(fallbackPath);
|
|
if (!isRoot() || invoking.uid === null || invoking.gid === null || invoking.uid === 0) {
|
|
return;
|
|
}
|
|
try {
|
|
fs.chownSync(filePath, invoking.uid, invoking.gid);
|
|
} catch {
|
|
// best-effort only
|
|
}
|
|
}
|
|
|
|
function writeFile(filePath: string, contents: string, mode = 0o755): void {
|
|
fs.writeFileSync(filePath, contents, { mode });
|
|
}
|
|
|
|
function generateRunScript(
|
|
platformServiceName: string,
|
|
runtimeHome: string,
|
|
nodePath: string,
|
|
projectRoot: string,
|
|
): string {
|
|
return [
|
|
'#!/bin/sh',
|
|
'set -eu',
|
|
`export SERVICE_NAME=${JSON.stringify(platformServiceName)}`,
|
|
`export HOME=${JSON.stringify(runtimeHome)}`,
|
|
'export PATH="/usr/local/bin:/usr/bin:/bin"',
|
|
`cd ${JSON.stringify(projectRoot)}`,
|
|
`exec ${JSON.stringify(nodePath)} ${JSON.stringify(path.join(projectRoot, 'dist/hostd/index.js'))}`,
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function generateRcdService(
|
|
platformServiceName: string,
|
|
projectRoot: string,
|
|
runScriptPath: string,
|
|
): string {
|
|
const name = `${platformServiceName}_hostd`;
|
|
const logPath = path.join(projectRoot, 'logs', `${platformServiceName}-hostd.log`);
|
|
return [
|
|
'#!/bin/sh',
|
|
'#',
|
|
`# PROVIDE: ${name}`,
|
|
'# REQUIRE: NETWORKING LOGIN',
|
|
'# KEYWORD: shutdown',
|
|
'',
|
|
'. /etc/rc.subr',
|
|
'',
|
|
`name="${name}"`,
|
|
`rcvar="${name}_enable"`,
|
|
'command="/usr/sbin/daemon"',
|
|
`pidfile="/var/run/${platformServiceName}-hostd-supervisor.pid"`,
|
|
'command_args="-P ${pidfile} -r -o ' +
|
|
JSON.stringify(logPath) +
|
|
' ' +
|
|
JSON.stringify(runScriptPath) +
|
|
'"',
|
|
'',
|
|
'load_rc_config $name',
|
|
': ${' + name + '_enable:="NO"}',
|
|
'',
|
|
'run_rc_command "$1"',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
const projectRoot = process.cwd();
|
|
const platform = getPlatform();
|
|
const nodePath = getNodePath();
|
|
const platformServiceName = SERVICE_NAME;
|
|
|
|
if (platform !== 'freebsd') {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'unsupported_platform', LOG });
|
|
process.exit(1);
|
|
}
|
|
if (!isRoot()) {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'requires_root', LOG });
|
|
throw new Error('setup_hostd_requires_root');
|
|
}
|
|
if (!commandExists('daemon')) {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'missing_daemon_bin', LOG });
|
|
throw new Error('missing_daemon_bin');
|
|
}
|
|
|
|
logger.info({ projectRoot, platformServiceName }, 'Setting up hostd service');
|
|
|
|
// Ensure dist exists
|
|
try {
|
|
buildProject(projectRoot);
|
|
} catch {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'build_failed', LOG });
|
|
process.exit(1);
|
|
}
|
|
|
|
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
|
chownIfInvokingContext(path.join(projectRoot, 'logs'), projectRoot);
|
|
|
|
const runScriptPath = path.join(projectRoot, `run-${platformServiceName}-hostd.sh`);
|
|
const rcdPath = `/usr/local/etc/rc.d/${platformServiceName}_hostd`;
|
|
|
|
writeFile(
|
|
runScriptPath,
|
|
generateRunScript(
|
|
platformServiceName,
|
|
PLATFORM_RUNTIME_HOME,
|
|
nodePath,
|
|
projectRoot,
|
|
),
|
|
);
|
|
chownIfInvokingContext(runScriptPath, projectRoot);
|
|
writeFile(
|
|
rcdPath,
|
|
generateRcdService(platformServiceName, projectRoot, runScriptPath),
|
|
);
|
|
|
|
// Enable + start
|
|
try {
|
|
execSync(`sysrc ${platformServiceName}_hostd_enable=YES`, { stdio: 'ignore' });
|
|
} catch (err) {
|
|
logger.warn({ err }, 'sysrc hostd enable failed');
|
|
}
|
|
try {
|
|
execSync(`service ${platformServiceName}_hostd restart`, { stdio: 'ignore' });
|
|
} catch (err) {
|
|
logger.warn({ err }, 'service hostd restart failed');
|
|
}
|
|
|
|
emitStatus('SETUP_HOSTD', {
|
|
STATUS: 'success',
|
|
SERVICE_NAME: `${platformServiceName}_hostd`,
|
|
RCD_PATH: rcdPath,
|
|
RUN_SCRIPT: runScriptPath,
|
|
LOG,
|
|
});
|
|
}
|