Fix dashboard template bugs, acme date parsing, wireguard sudoers match, and stale docs

- dashboard.html: Fix zones, leases, wg_status, cert key names, add services var
- server.py: Pass services to dashboard template via _get_service_status()
- lib/acme.py: Fix dead third date format (%Y%m%d%H%M%z) using astimezone(UTC)
- lib/wireguard.py: Add -- separator to cp command to match sudoers rule
- lib/nginx.py: Replace shallow dict.copy() with {**...} for DEFAULT_SSL
- AGENTS.md: Update test count 149 -> 154
- docs/api.md: Rename cert field expiry -> expires_at
This commit is contained in:
2026-05-08 19:11:54 +00:00
parent e2f56b8cc8
commit 65741644a3
26 changed files with 384 additions and 200 deletions
+12 -9
View File
@@ -2,7 +2,7 @@
## Privilege Model
The Vacuum Wall management WebUI (Flask application) runs as the unprivileged `vacuum-wall` system user. The application never runs as root. All privileged operations — firewall rule changes, nginx reloads, TLS certificate issuance — are executed through a restricted sudo whitelist defined at `/etc/sudoers.d/vacuum-wall`.
The Vacuum Wall management WebUI (Flask application) runs as the unprivileged `vacuum-wall` system user. 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 `vacuum-wall` 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 `vacuum-wall` user. If the WebUI process is compromised, an attacker is confined to the sudo whitelist surface rather than gaining unrestricted system access.
@@ -19,24 +19,26 @@ The file `/etc/sudoers.d/vacuum-wall` grants the `vacuum-wall` user passwordless
| 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 | `acme.sh` (via `bash -c`) | Let's Encrypt certificate issuance and renewal |
| File writes | `sudo cp` to `/etc/nginx/`, `/etc/dnsmasq.d/`, `/etc/wireguard/` | Copy rendered config files to system paths |
| Logs | `sudo journalctl --unit=*` | Query systemd journal for managed services |
| Certificates | (none) | acme.sh runs as the unprivileged `vacuum-wall` 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 |
| File writes | `sudo tee` | Write configuration data to protected paths |
| 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`).
- No wildcard entries grant shell access or arbitrary command execution.
- The `acme.sh` entry is restricted to certificate operations through an explicit `bash -c` wrapper that only passes acme-related arguments.
- `DEFAULT!/usr/bin/sudo` and `NOPASSWD` are used so the application never prompts for a password and cannot chain sudo calls.
- 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:vacuum-wall` restricts the secure path and disables TTY requirement.
## 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 using an `.htpasswd` file.
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`.
### Proxy Domains
@@ -84,6 +86,7 @@ The `vacuum-wall.service` unit file applies a comprehensive set of systemd sandb
| `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 |
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 data directory, and no ability to escalate privileges through kernel interfaces.