- bookshelf: Servarr v1 fork on the radarr template (4 tools). - unifi: portainer-shaped tooled app (7 tools, 9 ports), settings-merged config. - fail2ban: host-level security module mirroring firewall (service + restart-on, no container); ban actions preserved as source ufw/iptables and FLAGGED to be rewritten nftables-native before it actually bans. - marrytts: manifest-only plain container (no tools), like resolv-conf. All typecheck against the built @novox/mesh-sdk; service images digest-pinned. Held from merge pending the hq initialization reconciliation. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
// fail2ban's own code, in the module (novox/hq ADR 0044). The jails and the daemon are declared
|
|
// resources — the mesh writes /etc/fail2ban/jail.d/* and keeps fail2ban.service running (see
|
|
// module.json). This code exists only to read and steer the *live* state the daemon owns at
|
|
// runtime: which IPs are banned right now, and the manual ban/unban an operator reaches for. That
|
|
// state (the running bans, /var/lib/fail2ban's sqlite) is fail2ban's, not the mesh's — the mesh
|
|
// reconciles the config, never the ban list.
|
|
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const run = promisify(execFile);
|
|
|
|
export class Fail2banClient {
|
|
static fromEnv(_env: NodeJS.ProcessEnv = process.env): Fail2banClient {
|
|
return new Fail2banClient();
|
|
}
|
|
|
|
/** Overview of every jail, or the detailed status of one — currently-banned IPs and totals. */
|
|
async status(jail?: string): Promise<string> {
|
|
if (jail) {
|
|
const { stdout } = await run("sudo", ["fail2ban-client", "status", jail]);
|
|
return stdout;
|
|
}
|
|
const { stdout: overview } = await run("sudo", ["fail2ban-client", "status"]);
|
|
const match = overview.match(/Jail list:\s*(.+)/);
|
|
if (!match) return overview;
|
|
|
|
const jails = match[1].split(",").map((j) => j.trim()).filter(Boolean);
|
|
const parts: string[] = [overview.trimEnd(), ""];
|
|
for (const j of jails) {
|
|
const { stdout } = await run("sudo", ["fail2ban-client", "status", j]);
|
|
parts.push(`=== ${j} ===`, stdout.trimEnd(), "");
|
|
}
|
|
return parts.join("\n");
|
|
}
|
|
|
|
/** Manually ban an IP in a jail. Mutates live state, not a mesh-managed file. */
|
|
async ban(jail: string, ip: string): Promise<string> {
|
|
const { stdout } = await run("sudo", ["fail2ban-client", "set", jail, "banip", ip]);
|
|
return stdout;
|
|
}
|
|
|
|
/** Unban an IP from one jail, or from every jail when no jail is given. */
|
|
async unban(ip: string, jail?: string): Promise<string> {
|
|
const args = jail
|
|
? ["fail2ban-client", "set", jail, "unbanip", ip]
|
|
: ["fail2ban-client", "unban", ip];
|
|
const { stdout } = await run("sudo", args);
|
|
return stdout;
|
|
}
|
|
}
|