Files
vacuum-wall/docs/security.md
T
mteehan dcb581a359 Update documentation for config/ migration, new install model, and firewall config API
Reflect config/ vs data/ split, install-time Jinja2 templating, auto-discovered
paths, ZeroSSL/ACME support, and the new firewall save-then-apply API in all
reference docs.
2026-05-14 03:31:59 +00:00

10 KiB

Security Model

Privilege Model

The Vacuum Wall management WebUI (Flask application) runs as an unprivileged system user (default name: vacuum-wall, configurable via the USER_NAME environment variable at install time). The application never runs as root. All privileged operations — firewall rule changes, nginx reloads, dnsmasq config writes, WireGuard tunnel management — are executed through a restricted sudo whitelist defined at /etc/sudoers.d/vacuum-wall. ACME certificate operations via acme.sh are the exception: they run directly as the application user without sudo escalation, using webroot validation that doesn't require binding to privileged ports.

This design follows the principle of least privilege: only explicitly enumerated commands are permitted to escalate. There is no path to a full root shell from the application or the dedicated service user. If the WebUI process is compromised, an attacker is confined to the sudo whitelist surface rather than gaining unrestricted system access.

Sudo Whitelist

The file /etc/sudoers.d/vacuum-wall grants the configured system user passwordless sudo access to a strict set of commands. Each entry is scoped to a single binary with allowed arguments. The categories are:

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
Dnsmasq systemctl reload dnsmasq Apply updated dnsmasq configuration
Dnsmasq systemctl is-active dnsmasq Check dnsmasq service status
WireGuard wg-quick * WireGuard tunnel lifecycle (up, down, save, show)
WireGuard wg * WireGuard status and peer management
Certificates (none) acme.sh runs as the unprivileged service user directly; no sudo escalation is needed for certificate operations (webroot validation is used instead of standalone/TLS-ALPN)
File writes sudo cp to /etc/nginx/, /etc/nginx/conf.d/, /etc/nginx/snippets/, /etc/dnsmasq.d/, /etc/wireguard/ Copy rendered config files to system paths
File writes sudo tee to /etc/dnsmasq.d/vacuum-wall.conf Write dnsmasq configuration
File removal sudo rm for /etc/nginx/conf.d/vacuum-wall.conf, /etc/nginx/snippets/vacuum-wall-ssl.conf Clean up generated nginx config files
Directory creation sudo mkdir -p /etc/dnsmasq.d, sudo mkdir -p /etc/wireguard Ensure target directories exist
Logs sudo journalctl --unit=* -n * Query systemd journal for managed services
Logs sudo cat /var/log/nginx/* Read nginx access and error logs
Leases sudo cat /var/lib/dnsmasq/dnsmasq.leases Read dnsmasq lease table

Key safety properties:

  • Each Cmnd entry 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.
  • NOPASSWD is 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 $PROJECT_DIR/data/nginx/.htpasswd.

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-Security header is set with a long max-age to 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.
    • Content-Security-Policy rules can be customized per-domain via the configuration.

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 (ECDHE-ECDSA and ECDHE-RSA key exchange with AES-GCM and CHACHA20-POLY1305).
  • DH parameters: 2048-bit generated Diffie-Hellman parameters are used when ECDHE is not selected.
  • OCSP stapling is enabled for faster certificate validation.
  • ssl_prefer_server_ciphers can be toggled per-domain; the default is to let the client choose.

Systemd Hardening

The vacuum-wall.service unit file applies a comprehensive set of systemd sandboxing directives to isolate the WebUI process from the rest of the system:

Directive Value Effect
ProtectSystem strict Mounts the entire file system as read-only, except explicitly allowed paths
ProtectHome read-only Makes /home, /root, and /run/user inaccessible
ReadWritePaths $PROJECT_DIR/config, $PROJECT_DIR/data, and /tmp Both the application config directory and data directory, plus /tmp, are writable. The project path is templated at install time.
PrivateTmp yes Provides a private /tmp and /var/tmp namespace
NoNewPrivileges yes Prevents the process from gaining new privileges via setuid/setgid
IPAddressDeny all Drops all network traffic
IPAddressAllow localhost Allows only loopback communication (required to reach nginx upstream at 127.0.0.1:9090)
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

The User, Group, WorkingDirectory, ExecStart, and ReadWritePaths directives in the service unit are rendered from a Jinja2 template at install time with the configured USER_NAME and PROJECT_DIR.

This hardening ensures that even if the Flask application is compromised, the attacker is confined to a sandboxed environment with no direct network access, no write access outside the config and data directories, 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

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 in the service user's home directory under ~/.acme.sh/. The directory is owned by and writable only by the service user.

Private Key Protection

Private keys are never exposed through the WebUI API or returned in API responses. The API only returns certificate metadata such as domain names, validity dates, and renewal status. When a domain's certificate is needed by nginx, the rendered nginx configuration references the file paths managed by acme.sh (~/.acme.sh/<domain>/fullchain.cer and ~/.acme.sh/<domain>/<domain>.key), and nginx reads them directly through symbolic links or includes.

HSTS Enforcement

All HTTPS proxy domains 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 the domain and all 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. Weak ciphers, EXPORT grades, RC4, DES, 3DES, MD5, and null ciphers are explicitly excluded.