diff --git a/bastion/src/bastion/src/main.ts b/bastion/src/bastion/src/main.ts index 0b3fd51..5c30f70 100644 --- a/bastion/src/bastion/src/main.ts +++ b/bastion/src/bastion/src/main.ts @@ -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 = {}): Promise { // Load and populate config let config = loadConfig(overrides); @@ -135,6 +176,11 @@ export async function startBastion(overrides: Partial = {}): 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 = {}): 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}`); diff --git a/bastion/src/cli/src/commands/reprovision.ts b/bastion/src/cli/src/commands/reprovision.ts index 149807b..faa1ee4 100644 --- a/bastion/src/cli/src/commands/reprovision.ts +++ b/bastion/src/cli/src/commands/reprovision.ts @@ -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 {