fix: critical bugs + security hardening
Phase 1 (critical bugs): - Fix firewall import string-to-list bug (system_import.py) - Add rich rules removal in firewall config apply (handlers/firewall.py) Phase 2 (security hardening): - Restrict sudo wildcards to specific paths (sudoers.d/vacuum-walld) - Fix TOCTOU: use /run/vacuum-wall/ for temp files (nginx, dnsmasq, network handlers) - Remove unnecessary sudo from wg genkey/pubkey (handlers/wireguard.py) Phase 3 (validation): - Validate poll intervals > 0 (daemon/server.py) - Restrict sysctl to whitelisted parameters (handlers/network.py) Phase 4 (defensive programming): - Enforce shell=False in run() and run_proc() (lib/common.py) - Track issuance tasks for graceful shutdown (handlers/acme.py) - Add nginx template marker consistency tests (tests/test_system_import.py)
This commit is contained in:
@@ -43,16 +43,33 @@ logger = logging.getLogger(__name__)
|
||||
PROJECT_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
CONFIG_DIR = PROJECT_DIR / "config" / "network"
|
||||
DATA_DIR = PROJECT_DIR / "data" / "networkd"
|
||||
RUNTIME_DIR = Path("/run/vacuum-wall")
|
||||
|
||||
_ALLOWED_SYSCTL_KEYS: set[str] = {
|
||||
"net.ipv4.ip_forward",
|
||||
"net.ipv4.conf.all.forwarding",
|
||||
"net.ipv4.conf.all.accept_redirects",
|
||||
"net.ipv4.conf.default.accept_redirects",
|
||||
"net.ipv4.conf.all.send_redirects",
|
||||
"net.ipv4.conf.default.send_redirects",
|
||||
"net.ipv4.conf.all.rp_filter",
|
||||
"net.ipv4.icmp_echo_ignore_all",
|
||||
"net.ipv4.tcp_syncookies",
|
||||
}
|
||||
|
||||
|
||||
def _copy_and_reload(iface_name: str) -> None:
|
||||
"""Copy generated 99-<name>.network file to /etc/systemd/network/ and reload."""
|
||||
validate_interface_name(iface_name)
|
||||
src = DATA_DIR / f"99-{iface_name}.network"
|
||||
runtime_src = RUNTIME_DIR / f"99-{iface_name}.network"
|
||||
dst_dir = Path("/etc/systemd/network")
|
||||
RUNTIME_DIR.mkdir(exist_ok=True)
|
||||
runtime_src.write_text(src.read_text())
|
||||
run(["mkdir", "-p", str(dst_dir)], sudo=True)
|
||||
dst = dst_dir / f"99-{iface_name}.network"
|
||||
run(["cp", str(src), str(dst)], sudo=True)
|
||||
run(["cp", "--", str(runtime_src), str(dst)], sudo=True)
|
||||
runtime_src.unlink(missing_ok=True)
|
||||
|
||||
# Remove lower-priority .network files that match this interface
|
||||
# (they would override our config due to higher systemd priority)
|
||||
@@ -253,9 +270,12 @@ def apply_all(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
cleaned.append(f)
|
||||
|
||||
for f in generated:
|
||||
tmp = RUNTIME_DIR / f.name
|
||||
run(["cp", "--", str(f), str(tmp)], sudo=False)
|
||||
dst = sys_dir / f.name
|
||||
run(["mkdir", "-p", str(sys_dir)], sudo=True)
|
||||
run(["cp", str(f), str(dst)], sudo=True)
|
||||
run(["cp", "--", str(tmp), str(dst)], sudo=True)
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
_full_reload()
|
||||
|
||||
@@ -325,6 +345,8 @@ def set_sysctl(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
raise ValueError("'name' is required")
|
||||
if not re.match(r"^[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$", name):
|
||||
raise ValueError("'name' is not a valid sysctl key")
|
||||
if name not in _ALLOWED_SYSCTL_KEYS:
|
||||
raise ValueError("'name' is not a permitted sysctl key")
|
||||
value = str(body.get("value", "")).strip()
|
||||
if not value:
|
||||
raise ValueError("'value' is required")
|
||||
|
||||
Reference in New Issue
Block a user