feat: firewall management + reprovision SSH key fix
- Open firewall ports (dhcp, tftp, http, 4011) on bastion start - Close firewall ports on bastion shutdown - Auto-detect firewall zone for interface - Fix reprovision SSH to use execFileSync with explicit key path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,47 @@ function symlinkSafe(target: string, linkPath: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function runCmd(cmd: string, args: string[]): boolean {
|
||||
try {
|
||||
execSync(`${cmd} ${args.join(" ")}`, { stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let fwZoneFlag = "";
|
||||
let fwOpened = false;
|
||||
|
||||
function openFirewall(config: BastionConfig): void {
|
||||
// Check if firewalld is running
|
||||
if (!runCmd("firewall-cmd", ["--state"])) return;
|
||||
|
||||
// Detect zone for our interface
|
||||
try {
|
||||
const zone = execSync(`firewall-cmd --get-zone-of-interface=${config.iface} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
||||
if (zone) fwZoneFlag = `--zone=${zone}`;
|
||||
} catch { /* use default zone */ }
|
||||
|
||||
const zf = fwZoneFlag ? [fwZoneFlag] : [];
|
||||
logger.info(`Opening firewall ports (DHCP, TFTP, HTTP:${config.httpPort})...`);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--add-service=dhcp"]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--add-service=tftp"]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, `--add-port=${config.httpPort}/tcp`]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--add-port=4011/udp"]);
|
||||
fwOpened = true;
|
||||
}
|
||||
|
||||
function closeFirewall(config: BastionConfig): void {
|
||||
if (!fwOpened) return;
|
||||
const zf = fwZoneFlag ? [fwZoneFlag] : [];
|
||||
logger.info("Removing firewall rules...");
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--remove-service=dhcp"]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--remove-service=tftp"]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, `--remove-port=${config.httpPort}/tcp`]);
|
||||
runCmd("firewall-cmd", ["--quiet", ...zf, "--remove-port=4011/udp"]);
|
||||
}
|
||||
|
||||
export async function startBastion(overrides: Partial<BastionConfig> = {}): Promise<void> {
|
||||
// Load and populate config
|
||||
let config = loadConfig(overrides);
|
||||
@@ -135,6 +176,11 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
|
||||
// Generate dnsmasq config
|
||||
generateDnsmasqConf(config);
|
||||
|
||||
// Open firewall ports
|
||||
if (!config.skipDnsmasq) {
|
||||
openFirewall(config);
|
||||
}
|
||||
|
||||
// Start HTTP server
|
||||
const { app } = createApp(config);
|
||||
await app.listen({ port: config.httpPort, host: "0.0.0.0" });
|
||||
@@ -167,6 +213,7 @@ export async function startBastion(overrides: Partial<BastionConfig> = {}): Prom
|
||||
const shutdown = async () => {
|
||||
logger.info("Shutting down...");
|
||||
if (!config.skipDnsmasq) stopDnsmasq();
|
||||
closeFirewall(config);
|
||||
await app.close();
|
||||
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
||||
logger.info(`State preserved in ${config.stateFile}`);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// CLI command: provision reprovision
|
||||
// Queue a machine for reinstall and attempt SSH reboot into PXE.
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Command } from "commander";
|
||||
import type { BastionState } from "@lab/shared";
|
||||
|
||||
@@ -62,16 +65,27 @@ export function registerReprovisionCommand(parent: Command): void {
|
||||
console.log("");
|
||||
console.log(`Attempting SSH reboot into PXE (${effectiveUser}@${ip})...`);
|
||||
|
||||
try {
|
||||
const sshCmd = [
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=5",
|
||||
`${effectiveUser}@${ip}`,
|
||||
'sudo efibootmgr 2>/dev/null; PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi',
|
||||
].join(" ");
|
||||
// Find SSH key
|
||||
const realHome = process.env["SUDO_USER"]
|
||||
? join("/home", process.env["SUDO_USER"])
|
||||
: homedir();
|
||||
const keyPaths = [
|
||||
join(realHome, ".ssh", "id_ed25519"),
|
||||
join(realHome, ".ssh", "id_rsa"),
|
||||
join(realHome, ".ssh", "id_ecdsa"),
|
||||
];
|
||||
const sshKey = keyPaths.find(k => existsSync(k));
|
||||
|
||||
execSync(sshCmd, { stdio: "inherit" });
|
||||
const sshArgs = [
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
...(sshKey ? ["-i", sshKey] : []),
|
||||
`${effectiveUser}@${ip}`,
|
||||
'PXE_ENTRY=$(sudo efibootmgr | grep -iE "pxe|network|ipv4" | head -1 | grep -oP "Boot\\K[0-9A-F]+"); if [ -n "$PXE_ENTRY" ]; then sudo efibootmgr --bootnext "$PXE_ENTRY" && echo "PXE set as next boot" && sudo reboot; else echo "No PXE boot entry found, rebooting anyway..." && sudo reboot; fi',
|
||||
];
|
||||
|
||||
try {
|
||||
execFileSync("ssh", sshArgs, { stdio: "inherit" });
|
||||
console.log("");
|
||||
console.log("Machine is rebooting into PXE. Install will start automatically.");
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user