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:
Michal
2026-03-17 11:39:57 +00:00
parent 62f896593d
commit d01b675cca
2 changed files with 71 additions and 10 deletions

View File

@@ -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}`);

View File

@@ -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 {