/*
* SysDeck - Firewall Panel (v0.0.45)
* Author: Jeremy Anderson (https://dcos.net)
*
* v0.0.45 TRADEMARK SCRUB. Wording-only release — no functional changes.
* Per user directive: "you cannot say smoothwall and ipfire where merged
* into our fw script either. you can say logic derived from or influenced
* by these projects." Every reference to Smoothwall Express or IPFire now
* uses "takes influence from" / "logic derived from" instead of "merged"
* or "shipped". The sysdeck-fw backend, the 7 firewall templates, and the
* v0.0.44 service/port editor are unchanged. All 141 unit tests pass.
*
* v0.0.47 — SERVICE/PORT EDITOR MOVED TO ITS OWN SIDEBAR ENTRY.
* Per user directive: "we should move the service/ports editor to its
* own module entry for ease of access." The editor card that lived at
* the bottom of this panel since v0.0.44 has been lifted out into a
* first-class plugin — sysdeck-services at order 45. The bridge surface
* (bridge.firewall.services / service-info / set-service-port /
* restart-service) is unchanged; a new bridge.services proxy was added
* to bridge.js so the new panel has a clean API. The services() Promise
* in the parallel load below was removed because this panel no longer
* needs the inventory — it lives in the new Services panel. The
* renderServicePortEditor() function and the .btn-svc-save / .btn-svc-
* restart wireEvents handlers were removed from this file.
*
* v0.0.44 PUBLIC-SERVER VARIANTS + SERVICE/PORT EDITOR.
*
* - Three new public-server templates ship in this release:
* remote-admin.sh (SSH + Cockpit), public-webserver.sh
* (Caddy + Varnish + MariaDB), ai-llm.sh (Ollama + OpenWebUI
* + Hermes + Odysseus). They appear in the existing Templates
* card when the 'custom' backend is active — no new UI surface
* needed for selection.
* - Service/Port Editor card — runs bridge.firewall.services() to
* enumerate listening TCP ports on the host and cross-reference
* against the SERVICES_REGISTRY (ssh, cockpit, caddy, varnish,
* mariadb, ollama, openwebui, hermes, odysseus). Each registered
* service shows: current port from its config file, the listening
* ports actually active, the systemd unit, and an editable port
* input. Clicking Save edits the config file atomically (tmpfile
* + fsync + rename) and runs `systemctl restart` on the service.
* Unmapped listeners (ports with no matching registry entry) are
* shown in a separate block so the operator can spot services
* the editor doesn't yet know about.
*
* v0.0.31 REWRITE — PREVIOUS VERSION WAS READ-ONLY.
*
* The v0.0.30 panel could only list active nftables rules. v0.0.31
* turns it into a full firewall manager:
*
* - Template selector — operator picks from installed templates
* under /usr/share/sysdeck/firewall/templates/ (vps-webserver.sh,
* no-services.sh, plus any operator-dropped *.sh). Each template
* is shown with its description and detected services.
* - Apply / Stop / Restart buttons — invoke the template's start /
* stop / restart action via the bridge under the cockpit
* superuser channel (polkit). No `sudo` shell-out from JS.
* - Service detection preview — runs the template's `detect`
* action and renders the inventory (OS, interface, services
* detected) before applying.
* - Live ban-list table — ssh_abuse / port_scanners / connlimit_abuse
* sets with per-IP Unban buttons and a Clear All button.
* - Active ruleset table — refreshed after each mutating operation
* so the operator sees the new state immediately.
*
* v0.0.36 BACKEND DROPDOWN + SECURITY CARD.
*
* - Backend selector — operator picks between custom / cilium /
* sysdeck-fw. The bridge probes availability (cilium
* installed? nftables installed? kernel BPF features?) and shows
* an install hint if missing. The "Install" button triggers
* bridge.firewall.installBackend (delegates to packages module).
* - Cilium-specific sections — when cilium is the active backend,
* the panel renders Cilium Status / Endpoints / Policy cards in
* place of the nftables ruleset table.
* - Security Card — renders the CVE-derived hardening checklist
* (bridge.firewall.securityHardening). Documents the lessons
* applied from Webmin, Cockpit, Ajenti, ISPConfig, Virtualmin,
* cPanel, Plesk, CyberPanel, aaPanel, CloudPanel, HestiaCP,
* VestaCP, Froxlor, InterWorx, BrainyCP, DirectAdmin, CWP CVE
* disclosures. Full table in docs/SECURITY-HARDENING.md.
* - Excluded backends info — explains why UFW, fwbuilder, iptables-
* legacy, iptables-nft, Shorewall, Smoothwall Express (trademark),
* and IPFire (trademark) are not in the dropdown. We took influence
* from Smoothwall Express and IPFire for the sysdeck-fw backend;
* we do not ship templates called "smoothwall" or "ipfire".
*
* Mutating ops go through bridge.firewall.apply / stop / restart / ban /
* unban / clearBans / switchBackend / installBackend / ciliumPolicyApply,
* which pass { superuser: 'try' } to cockpit.spawn. The cockpit bridge
* prompts the operator for auth via polkit; the org.sysdeck.firewall.modify
* action (shipped since v0.0.17, extended in v0.0.36 to authorize cilium
* + cilium-agent + helm) authorizes the binaries. This is the "cockpit
* way" per user directive v0.0.31.
*/
export async function mount(panel, { bridge, EventBus }) {
panel.innerHTML = renderSkeleton();
// v0.0.47: services inventory no longer loaded here — the editor
// moved to its own sidebar entry (sysdeck-services at order 45).
const [backendsResp, templates, status, rules, chains, hardening] = await Promise.all([
safe(bridge.firewall.backends(), { active: 'custom', backends: [], excluded: [] }),
safe(bridge.firewall.templates(), []),
safe(bridge.firewall.status(), { state: 'unavailable', bans: {} }),
safe(bridge.firewall.listRules(), []),
safe(bridge.firewall.listChains(), []),
safe(bridge.firewall.securityHardening(), { applied: [], cves_reviewed: [] }),
]);
const activeBackend = backendsResp?.active || 'custom';
const backends = backendsResp?.backends || [];
const excluded = backendsResp?.excluded || [];
const activeTemplate = status?.active_template || null;
const state = status?.state || 'unavailable';
const bans = status?.bans || {};
const bannedIpCount = status?.banned_ip_count || 0;
const isCilium = activeBackend === 'cilium';
panel.innerHTML = `
`;
// v0.0.43: Store backends data for getSelectedTemplate() to access
// (the Apply button handler needs to know which backend is active
// to determine which template to apply).
window.__sysdeckFirewallBackends = backends;
window.__sysdeckFirewallActiveBackend = activeBackend;
wireEvents(panel, { bridge, EventBus });
EventBus.emit('firewall.loaded', {
ruleCount: rules.length, state, bannedIpCount, activeBackend,
});
}
// ── Render helpers ──────────────────────────────────────────────────
function renderBackendSelector(backends, excluded, activeBackend, templates, activeTemplate) {
if (!backends || !backends.length) {
return `
Firewall Backend
No firewall backends available. This indicates a
broken install — re-install the sysdeck
package.
`;
}
// v0.0.43: Find the active backend object to determine whether it
// has its own template (cilium → cilium, sysdeck-fw → sysdeck-fw).
// If it does, the template selector is hidden — the backend IS the
// template. Only 'custom' shows the template selector.
const activeBackendObj = backends.find((b) => b.id === activeBackend) || backends[0];
const backendHasTemplate = activeBackendObj && activeBackendObj.template;
const options = backends.map((b) => {
const isActive = b.id === activeBackend;
const installed = b.available?.installed;
const techBadge = b.ebpf
? 'eBPF'
: 'nftables';
const statusBadge = installed
? 'installed'
: 'not installed';
return `
`;
}).join('');
const excludedItems = excluded.map((e) => `
${escapeHtml(e.id)} — ${escapeHtml(e.reason)}
`).join('');
return `
Firewall Backend (${backends.length})
${options}
Switching backend stops the previous backend cleanly
before applying the new one.
${backendHasTemplate
? `The ${escapeHtml(activeBackendObj.name)} backend uses its own template (${escapeHtml(activeBackendObj.template)}) — no template selection needed.`
: 'The custom backend lets you pick from the basic nftables templates below.'}
${excluded.length ? `
Excluded backends (${excluded.length}) — click to expand
`;
}
function renderServicesLinkCard() {
// v0.0.47: the Service / Port Editor moved to its own sidebar entry
// (sysdeck-services at order 45). This card is a signpost — it tells
// the operator where to find the editor and what it does. Keeping
// a stub here preserves the workflow for operators who used to
// scroll to the bottom of the Firewall panel for port edits.
return `
Service / Port Editor
moved
The service/port editor has been promoted to its own
sidebar entry — Service / Ports at
order 45 — for ease of access. It enumerates every
listening TCP socket on the host via
ss -tlnp (with a
/proc/net/tcp fallback), cross-references
against the SERVICES_REGISTRY in
bridge/firewall.py, and lets you edit
the port in the service's config file with an atomic
write + systemctl restart.
Click Service / Ports in the sidebar
to open the editor. The bridge surface
(bridge.firewall.services /
service-info /
set-service-port /
restart-service) is unchanged from
v0.0.44; a new bridge.services proxy
was added in v0.0.47 so the new panel has a clean
API surface.
`;
}
function renderTemplateSelector(templates, activeTemplate, state, activeBackend, backends) {
// v0.0.43: If the active backend has its own template (cilium → cilium,
// sysdeck-fw → sysdeck-fw), DON'T render the template selector at all —
// the backend IS the template. This fixes the v0.0.36 logic flaw where
// two independent lists (backend + template) didn't coordinate.
const activeBackendObj = backends?.find((b) => b.id === activeBackend);
if (activeBackendObj?.template) {
return ''; // backend has its own template — selector not needed
}
// For the 'custom' backend, filter templates to show ONLY the basic
// nftables templates (vps-webserver, no-services). Exclude cilium +
// sysdeck-fw — those are backend-specific and would conflict if applied
// while the custom backend is active.
const backendTemplates = new Set(
(backends || [])
.filter((b) => b.template) // backends with their own template
.map((b) => b.template)
);
const filteredTemplates = (templates || []).filter((t) =>
!backendTemplates.has(t.name)
);
if (!filteredTemplates.length) {
return `
Templates
No firewall templates found under
/usr/share/sysdeck/firewall/templates/.
Install the sysdeck package to ship the
default templates (vps-webserver.sh, no-services.sh),
or drop your own *.sh file there.