refactor: introduce two-user daemon architecture with socket-based communication

- Add daemon/ module with aiohttp server, sync client, and handler registry
- Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard)
- Add system/acme-deploy.py, vacuum-walld sudoers and systemd service
- Update API routes to use daemon client instead of lib/ directly
- Update lib/, tests/, and webui/ for new architecture
- Update docs and deployment scripts
This commit is contained in:
2026-05-27 23:38:23 +00:00
parent 5ac69dfa7e
commit 200e078bc5
39 changed files with 4671 additions and 1810 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""acme-deploy.py — Deploy hook for acme.sh (Vacuum Wall).
Called by acme.sh after every successful certificate issue or renewal.
Reloads nginx via the daemon API so acme.sh never touches sudo directly.
"""
import logging
import os
import sys
try:
import requests_unixsocket
from daemon.client import post
logging.basicConfig(level=logging.INFO)
project_dir = os.environ.get("INSTALL_DIR", os.path.dirname(os.path.dirname(__file__)))
socket_path = os.environ.get(
"VACUUM_WALLD_SOCKET",
os.path.join(project_dir, "data", "daemon.sock"),
)
post("/nginx/reload", socket_path=socket_path)
sys.exit(0)
except Exception as exc:
logging.error("acme-deploy hook failed: %s", exc)
sys.exit(0)
+2 -34
View File
@@ -1,34 +1,2 @@
# Defaults directives
Defaults:{{ USER_NAME }} !requiretty
Defaults:{{ USER_NAME }} secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Firewall management
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/firewall-cmd *
# Nginx management
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -t
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/conf.d/
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/snippets/
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/conf.d/vacuum-wall.conf
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/snippets/vacuum-wall-ssl.conf
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/chown root\:root /etc/nginx/snippets/vacuum-wall-ssl.conf
# Dnsmasq management
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl reload dnsmasq
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl is-active dnsmasq
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/dnsmasq.d/
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/dnsmasq/dnsmasq.leases
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/tee /etc/dnsmasq.d/vacuum-wall.conf
# WireGuard management
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg-quick *
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg *
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/wireguard/
# Misc
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/dnsmasq.d
{{ USER_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/wireguard
# WebUI user ({{ USER_NAME }}) no longer has sudo access.
# Privileged operations are handled by vacuum-walld via the daemon API.
+38
View File
@@ -0,0 +1,38 @@
# Defaults directives
Defaults:{{ USER_DAEMON_NAME }} !requiretty
Defaults:{{ USER_DAEMON_NAME }} secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Firewall management
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/firewall-cmd *
# Nginx management
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/sbin/nginx -t
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/conf.d/
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/snippets/
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/conf.d/vacuum-wall.conf
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/snippets/vacuum-wall-ssl.conf
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/chown root\:root /etc/nginx/snippets/vacuum-wall-ssl.conf
# Dnsmasq management
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl reload dnsmasq
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/systemctl is-active dnsmasq
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/dnsmasq.d/
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/dnsmasq/dnsmasq.leases
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/tee /etc/dnsmasq.d/vacuum-wall.conf
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/dnsmasq.d
# WireGuard management
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg-quick *
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/wg *
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/wireguard/
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/chown root\:root /etc/wireguard/wg0.conf
# Network interface queries
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/sbin/ip -o link show
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/sbin/ip -o addr show
# Misc
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
+3 -2
View File
@@ -1,13 +1,14 @@
[Unit]
Description=Vacuum Wall Management WebUI
Documentation=https://github.com/wall/vacuum-wall
After=network.target firewalld.service nginx.service dnsmasq.service
Requires=vacuum-walld.service
After=network.target firewalld.service nginx.service dnsmasq.service vacuum-walld.service
Wants=firewalld.service
[Service]
Type=simple
User={{ USER_NAME }}
Group={{ USER_NAME }}
Group={{ USER_GROUP }}
WorkingDirectory={{ PROJECT_DIR }}
ExecStart={{ PROJECT_DIR }}/.venv/bin/python webui/server.py
Restart=on-failure
+42
View File
@@ -0,0 +1,42 @@
[Unit]
Description=Vacuum Wall Daemon (privileged backend)
Documentation=https://github.com/wall/vacuum-wall
After=network.target firewalld.service
Wants=firewalld.service
[Service]
Type=simple
User={{ USER_DAEMON_NAME }}
Group={{ USER_GROUP }}
WorkingDirectory={{ PROJECT_DIR }}
ExecStart={{ PROJECT_DIR }}/.venv/bin/python -m daemon
Restart=on-failure
RestartSec=5
Environment=PATH=/usr/local/bin:/usr/bin
Environment=PYTHONUNBUFFERED=1
Environment=ACME_HOME={{ PROJECT_DIR }}/data/acme
Environment=HOME={{ PROJECT_DIR }}
# Security hardening
ProtectSystem=strict
ReadWritePaths={{ PROJECT_DIR }} /tmp
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectHostname=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
RestrictNamespaces=yes
LockPersonality=yes
SystemCallFilter=@system-service
PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target