12 KiB
Security Model
Privilege Model
Vacuum Wall uses two distinct system users bridged by a shared group (the WebUI user's primary group):
vacuum-walld(daemon user): Runs thevacuum-walldbackground daemon, which is the only process with sudo access. The daemon communicates with the WebUI over a Unix socket atdata/daemon.sock. All privileged operations — firewall rule changes, nginx reloads, dnsmasq config writes, WireGuard tunnel management — are executed by the daemon through a restricted sudo whitelist at/etc/sudoers.d/vacuum-walld.- WebUI user (default: repo owner in
--devmode): Runs the Flask management WebUI. Has zero sudo access. If the WebUI process is compromised, an attacker cannot invoke sudo directly — they are confined to the sandboxed Flask process with no privilege escalation path.
ACME certificate operations via acme.sh run as the WebUI user — not as root, and not as the daemon user. The automated renewal timer (vacuum-wall-acme.timer) runs acme.sh --cron as {{ USER_NAME }}. When triggered from the WebUI or daemon, acme.sh also runs as the non-root process invoking it, using webroot validation that does not require binding to privileged ports.
This design follows the principle of least privilege: only the daemon process holds sudo access, and only for explicitly enumerated commands. The WebUI user is completely isolated from sudo.
Communication Between WebUI and Daemon
The WebUI communicates with the daemon via synchronous HTTP requests over a Unix socket (data/daemon.sock), owned by vacuum-walld:<group> with mode 0660. The shared group membership allows the WebUI user to connect to the socket. The daemon runs an aiohttp server that routes requests to handler modules (daemon/handlers/*.py), which execute the privileged commands.
Sudo Whitelist
The file /etc/sudoers.d/vacuum-walld grants the daemon user (vacuum-walld) passwordless sudo access to a strict set of commands. The WebUI user has no sudo access. Each daemon entry is scoped to a single binary with allowed arguments:
| Category | Whitelisted Command | Purpose |
|---|---|---|
| Firewall | firewall-cmd * |
All firewalld operations (zone management, rules, services, ports) |
| Nginx | nginx -s reload |
Graceful nginx configuration reload |
| Nginx | nginx -t |
Nginx configuration syntax validation |
| Nginx file ops | cp -- * /etc/nginx/, /etc/nginx/conf.d/, /etc/nginx/snippets/ |
Copy rendered config files to system paths |
| Nginx file ops | rm /etc/nginx/conf.d/vacuum-wall.conf, /etc/nginx/snippets/vacuum-wall-ssl.conf |
Clean up generated nginx config files |
| Nginx file ops | chown root:root /etc/nginx/snippets/vacuum-wall-ssl.conf |
Ensure correct ownership of SSL snippet |
| Dnsmasq | systemctl reload dnsmasq |
Apply updated dnsmasq configuration |
| Dnsmasq | systemctl is-active dnsmasq |
Check dnsmasq service status |
| Dnsmasq file ops | cp -- * /etc/dnsmasq.d/ |
Copy rendered config files |
| Dnsmasq file ops | tee /etc/dnsmasq.d/vacuum-wall.conf |
Write dnsmasq configuration |
| Dnsmasq file ops | mkdir -p /etc/dnsmasq.d |
Ensure target directory exists |
| Dnsmasq leases | cat /var/lib/dnsmasq/dnsmasq.leases |
Read dnsmasq lease table |
| WireGuard | wg-quick * |
WireGuard tunnel lifecycle (up, down, save, show) |
| WireGuard | wg * |
WireGuard status and peer management |
| WireGuard file ops | cp -- * /etc/wireguard/ |
Copy rendered config files |
| WireGuard file ops | chown root:root /etc/wireguard/wg0.conf |
Ensure correct ownership of WG config |
| Certificates | (none) | acme.sh runs as the non-root service user directly; no sudo escalation is needed (webroot validation is used) |
| Network queries | ip -o link show |
List network interfaces |
| Network queries | ip -o addr show |
List IP addresses on interfaces |
| Logs | journalctl --unit=* -n * |
Query systemd journal for managed services |
| Logs | cat /var/log/nginx/* |
Read nginx access and error logs |
Key safety properties:
- Each
Cmndentry specifies the full path to the binary (e.g.,/usr/bin/firewall-cmd). - Wildcard entries exist only for commands where the full argument space is needed (
firewall-cmd *,wg-quick *,wg *), but none grant shell access or arbitrary command execution. NOPASSWDis used so the application never prompts for a password.Defaults:<user>restricts the secure path and disables TTY requirement.- The sudoers file is rendered from a Jinja2 template at install time, substituting the configured user name.
Web Security
Management Interface
The Flask WebUI binds exclusively to 127.0.0.1:9090. It is not exposed directly to any network interface. All external access to the management UI is routed through an nginx reverse proxy on the designated management domain, which provides SSL termination and HTTP Basic Authentication. The .htpasswd file is stored at data/nginx/.htpasswd.
The management interface does not set security hardening headers (e.g., X-Content-Type-Options, X-Frame-Options, HSTS). It relies on nginx basic authentication, SSL termination, and the systemd sandbox for its security boundary.
Proxy Domains
Every proxied domain configured in Vacuum Wall enforces:
- HTTP-to-HTTPS redirect — All HTTP requests return a 301 Permanent Redirect to the HTTPS equivalent.
- HTTP Strict Transport Security (HSTS) — The
Strict-Transport-Securityheader is set with a long max-age andincludeSubDomainsto prevent downgrade attacks. - Security headers on all proxied responses:
X-Content-Type-Options: nosniff— Prevents MIME-type sniffing.X-Frame-Options: DENY— Prevents clickjacking via iframes.X-XSS-Protection: 1; mode=block— Enables browser XSS filtering.Referrer-Policy: strict-origin-when-cross-origin— Limits referrer information leakage.
Additional proxy headers (extra_headers in the domain config) are delivered to the upstream backend via nginx proxy_set_header directives — they are not sent as response headers to clients.
TLS Configuration
The default nginx SSL configuration enforces modern TLS only:
- Protocols: TLSv1.2 and TLSv1.3. Older protocols (SSLv3, TLSv1.0, TLSv1.1) are disabled.
- Cipher suites: A curated set of AEAD ciphers using ECDHE key exchange (ECDHE-ECDSA and ECDHE-RSA with AES-GCM and CHACHA20-POLY1305). No non-ECDHE ciphers are included.
- ssl_prefer_server_ciphers defaults to
off(client chooses). - Session settings:
ssl_session_timeout 1d,ssl_session_cache shared:TLS:10m,ssl_session_tickets off.
Systemd Hardening
Both vacuum-wall.service (WebUI) and vacuum-walld.service (daemon) apply comprehensive systemd sandboxing directives to isolate their processes from the rest of the system:
| Directive | Value | Effect |
|---|---|---|
ProtectSystem |
strict |
Mounts the entire file system as read-only, except explicitly allowed paths |
ReadWritePaths |
project dir, /tmp, and (WebUI only) config/, data/ subdirs |
The project directory and runtime paths are writable |
PrivateTmp |
yes |
Provides a private /tmp and /var/tmp namespace |
NoNewPrivileges |
yes |
Prevents the process from gaining new privileges via setuid/setgid |
PrivateDevices |
yes |
Hides all device files under /dev |
ProtectKernelTunables |
yes |
Makes /proc/sys, /sys, and /proc/sysrq-trigger read-only |
ProtectKernelModules |
yes |
Disables init_module and finit_module syscalls |
ProtectControlGroups |
yes |
Mounts /sys/fs/cgroup as read-only |
ProtectHostname |
yes |
Prevents the process from changing the system hostname |
RestrictNamespaces |
yes |
Prevents creating new namespaces |
RestrictSUIDSGID |
yes |
Removes setuid/setgid bits from newly created files |
LockPersonality |
yes |
Prevents changing the execution domain |
MemoryDenyWriteExecute |
yes |
Prevents creating memory regions that are both writable and executable |
SystemCallFilter |
@system-service |
Allows only a curated set of system calls safe for services |
RestrictRealtime |
yes |
Prevents the process from acquiring realtime scheduling priorities |
RestrictAddressFamilies |
AF_UNIX AF_INET AF_INET6 |
Restricts available address families |
IPAddressDeny |
any |
Drops all network traffic by default |
IPAddressAllow |
localhost |
Allows only loopback communication (required to reach the other process at 127.0.0.1) |
The WebUI unit additionally restricts address families and denies all IP traffic except to localhost — it cannot reach any external network interface. Both units use template variables ({{ USER_NAME }}, {{ USER_GROUP }}, {{ USER_DAEMON_NAME }}, {{ PROJECT_DIR }}) rendered at install time.
This hardening ensures that even if either process is compromised, the attacker is confined to a sandboxed environment with no direct network access, no write access outside the project directory, and no ability to escalate privileges through kernel interfaces.
Network Security
Default Deny
The firewalld default zone policy is set to deny all incoming traffic. Only explicitly allowed services and ports are accessible. Outbound traffic is permitted by default.
Zone-Based Traffic Isolation
The lib/firewall module is a generic firewalld parser with no hardcoded zone definitions. Zone structure is defined declaratively in config/firewall/config.json at runtime. A typical deployment uses:
| Zone | Interface | Purpose | Behavior |
|---|---|---|---|
external |
WAN (e.g., eth0) |
Untrusted Internet-facing | Only essential services (HTTPS, WireGuard) are open. ICMP echo is rate-limited. |
internal |
LAN (e.g., eth1) |
Trusted local network | DHCP and DNS served to clients. Masquerade (NAT) enabled for outbound Internet access. All outbound traffic from the LAN is allowed. |
vpn |
WireGuard (wg0) |
WireGuard tunnel traffic | Semi-trusted. Firewall rules control which internal services VPN peers can reach. Traffic to the LAN is restricted to specific services and ports. |
trusted / loopback |
lo |
Localhost communication | unrestricted; used for the Flask-to-nginx management proxy. |
| Custom zones | — | DMZ, guest networks, etc. | Additional zones can be created to isolate specific network segments with their own rule sets. |
IP Forwarding and NAT
IP forwarding (net.ipv4.ip_forward = 1) is enabled system-wide to allow routing between zones (LAN to Internet, VPN to LAN). However, actual traffic flow is controlled by firewalld rules. Masquerade is enabled on the internal zone so that LAN clients get NAT translation when accessing the Internet through the Vacuum Wall router.
Certificate Security
acme.sh Integration
Certificate management is handled by acme.sh, which stores all certificates and private keys under PROJECT_DIR/data/acme/ (set via the ACME_HOME environment variable). The directory is owned by and writable only by the service users.
Private Key Protection
Private key material is never exposed through the WebUI API. The API returns certificate metadata such as domain names, validity dates, file paths, and renewal status. File paths are returned so downstream tooling (nginx, cert management) can reference them. When a domain's certificate is needed by nginx, the rendered nginx configuration references the acme.sh file paths directly via ssl_certificate and ssl_certificate_key directives — no symlinks are created.
HSTS Enforcement
All HTTPS proxy domains (excluding the management interface) have HTTP Strict Transport Security enabled at the nginx layer with a long max-age and the includeSubDomains directive. This ensures browsers always use HTTPS for proxied domains and their subdomains, preventing SSL stripping attacks.
Modern TLS Only
As noted in the Web Security section, the default SSL snippet enforces TLSv1.2 and TLSv1.3 with strong AEAD cipher suites using ECDHE key exchange. The cipher suite list excludes weak ciphers, EXPORT grades, RC4, DES, 3DES, MD5, and null ciphers.