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:
+72
-14
@@ -105,6 +105,34 @@ Poll intervals are configurable via `VACUUM_WALL_POLL_INTERVALS` env var (`firew
|
||||
|
||||
On collector failure during a poll, no broadcast is sent (avoids noisy ticks). State data is set to `None`.
|
||||
|
||||
## System Config Import
|
||||
|
||||
On daemon startup, `lib/system_import.py` reconciles live system configurations
|
||||
with the declarative JSON configs. This ensures that configurations created
|
||||
by `scripts/install.sh` or edited manually in system files are imported into
|
||||
the JSON source of truth, preventing drift.
|
||||
|
||||
When `vacuum-walld` starts, it calls `import_all()` which runs each subsystem
|
||||
import function:
|
||||
|
||||
- **`import_dnsmasq`**: Parses `/etc/dnsmasq.d/vacuum-wall.conf` (managed
|
||||
block between comment markers) → `config/dnsmasq/config.json`. Only writes
|
||||
if config doesn't exist or differs.
|
||||
- **`import_wireguard`**: Parses `/etc/wireguard/wg0.conf` →
|
||||
`config/wireguard/config.json`. Skips if configs match.
|
||||
- **`import_networkd`**: Parses `/etc/systemd/network/99-*.network` files
|
||||
(install-time files) → `config/network/config.json`. Only adds/updates
|
||||
interfaces; doesn't remove interfaces without a file (they may be pending apply).
|
||||
- **`import_nginx`**: Parses `data/nginx/sites-enabled/*.conf` →
|
||||
`config/nginx/config.json`. Only touches vacuum-wall-managed files
|
||||
(identified by `# Auto-generated by Vacuum Wall` header). Skips `_acme-challenge.conf`.
|
||||
- **`import_firewall`**: Runs `sudo firewall-cmd --list-all-zones` →
|
||||
`config/firewall/config.json`. Only writes if no config file exists
|
||||
(firewalld state always takes precedence).
|
||||
|
||||
Import failures are silently logged as warnings — they never abort daemon startup.
|
||||
The returned list of updated subsystems is logged for debugging.
|
||||
|
||||
## Cross-Subsystem Sync Event Bus
|
||||
|
||||
When a subsystem's configuration changes, related subsystems are automatically
|
||||
@@ -116,28 +144,33 @@ subsystems — no handler calls into another handler's logic directly.
|
||||
1. A mutation handler saves its config (e.g., adding a DHCP range).
|
||||
2. The handler emits a `SyncEvent` on the event bus.
|
||||
3. Subscribers react by updating related subsystem configs:
|
||||
- **DnsToFirewallSync**: Adds `dhcp`, `dns` services and `masquerade` to the
|
||||
firewall zone for each interface serving a DHCP range.
|
||||
- **DnsToFirewallSync**: Adds `dhcp`, `dns` services to the firewall zone
|
||||
for each interface serving a DHCP range. Back-propagates gateway (interface
|
||||
IP) into DHCP ranges so clients receive their default route.
|
||||
- **WgToFirewallSync**: Creates or updates a `vpn` firewall zone with
|
||||
WireGuard interface and UDP 51820 rich rule.
|
||||
- **FirewallToDhcpSync**: Detects stale DHCP ranges for interfaces not in
|
||||
any zone (logs warnings, does not auto-remove).
|
||||
- **NetworkToAllSync**: Suggests DHCP ranges and syncs firewall zone
|
||||
interface assignments when network config changes.
|
||||
WireGuard interface, masquerade, UDP 51820 rich rule, and inter-zone
|
||||
accept rules for each peer's allowed_ips subnets. Cleans up WireGuard-created
|
||||
entries when no active peers exist.
|
||||
- **FirewallToDhcpSync**: Removes stale DHCP ranges for interfaces no longer
|
||||
in any zone. Ensures DHCP ranges on masquerade-enabled zones carry the
|
||||
gateway (interface IP). Logs warnings for zones with dhcp service but no range.
|
||||
- **NetworkToAllSync**: Suggests DHCP ranges for static-IP interfaces without
|
||||
ranges. Syncs firewall zone interface assignments — adding new interfaces
|
||||
and removing stale ones no longer in network config.
|
||||
4. The handler refreshes state for the originating subsystem plus all
|
||||
transitively affected subsystems.
|
||||
transitively affected subsystems.
|
||||
|
||||
### Guard Rails
|
||||
|
||||
- **Idempotency**: Each subscriber reads current state, computes desired state,
|
||||
writes the diff. Running twice is safe.
|
||||
writes the diff. Running twice is safe.
|
||||
- **No loops**: The event bus tracks `(subsystem, action)` per dispatch cycle.
|
||||
Re-entrant emits for the same key are silently dropped.
|
||||
Re-entrant emits for the same key are silently dropped.
|
||||
- **Firewall-cmd separation**: Sync subscribers only write JSON config. They
|
||||
do NOT call `firewall-cmd`. The user clicks "Apply" on the firewall page to
|
||||
push to firewalld.
|
||||
do NOT call `firewall-cmd`. The user clicks "Apply" on the firewall page to
|
||||
push to firewalld.
|
||||
- **Error handling**: Subscriber exceptions are caught, logged as warnings,
|
||||
and do NOT abort the originating handler.
|
||||
and do NOT abort the originating handler.
|
||||
|
||||
### Frontend Impact
|
||||
|
||||
@@ -145,6 +178,28 @@ Minimal. The sync happens transparently in the backend. The "pending changes"
|
||||
indicator on the firewall page will show pending when DHCP or WireGuard saves
|
||||
(since sync writes JSON but does not call firewall-cmd).
|
||||
|
||||
## System Config Import
|
||||
|
||||
On daemon startup, `vacuum-walld` runs `import_all()` from `lib/system_import.py`
|
||||
to reconcile any drift between system configuration files and the declarative
|
||||
JSON configs. This is invoked from `daemon/server.py` during initialization.
|
||||
|
||||
Each subsystem import function parses the corresponding live system config and
|
||||
updates the JSON config if they differ:
|
||||
|
||||
| Subsystem | Source | Condition |
|
||||
|---|---|---|
|
||||
| dnsmasq | `/etc/dnsmasq.d/vacuum-wall.conf` | Always — parses managed block between markers |
|
||||
| firewall | `firewall-cmd --list-all-zones` | Only if no JSON config exists yet |
|
||||
| WireGuard | `/etc/wireguard/wg0.conf` | Always — parses INI format |
|
||||
| networkd | `/etc/systemd/network/99-*.network` | Always — parses INI files |
|
||||
| nginx | `data/nginx/sites-enabled/*.conf` | Always — parses generated server blocks |
|
||||
|
||||
All imports are **idempotent** and **non-destructive**: they only write when
|
||||
configs differ, skip on failure (logged as warnings), and never abort daemon
|
||||
startup. This ensures that manual edits to system files (e.g., during install
|
||||
or troubleshooting) are reconciled into the declarative JSON source of truth.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Config — Declarative Settings
|
||||
@@ -185,7 +240,9 @@ data/
|
||||
├── networkd/ # Generated 50-<name>.network files
|
||||
```
|
||||
|
||||
Both `config/` and `data/` reside within the project directory. The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to both directories, while keeping the rest of the filesystem read-only. The `INSTALL_DIR` value is templated into the service unit at install time.
|
||||
Both `config/` and `data/` reside within the project directory. The systemd service unit's `ReadWritePaths` directive grants the processes write access to these directories, while keeping the rest of the filesystem read-only. The `INSTALL_DIR` value is templated into the service unit at install time.
|
||||
|
||||
The daemon uses a **runtime directory** at `/run/vacuum-wall` (created by systemd `RuntimeDirectory=`) for secure temporary files during config apply. `tempfile.NamedTemporaryFile` writes to this directory before `sudo cp` moves files to their final destination, eliminating TOCTOU symlink races that would exist with `/tmp`. The directory is automatically removed on service stop.
|
||||
|
||||
## File System Layout
|
||||
|
||||
@@ -199,6 +256,7 @@ The following file system locations are used for integration with system service
|
||||
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `config/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
|
||||
| `/etc/systemd/network/50-<name>.network` | Generated systemd-networkd drop-in files. Written from `config/network/config.json`, one per interface. | Vacuum Wall (lib/network.py) |
|
||||
| `/etc/sudoers.d/vacuum-walld` | Sudo whitelist for the daemon user. Defines all permitted privilege escalations. | Install script (rendered from Jinja2 template) |
|
||||
| `/run/vacuum-wall` | Runtime directory for secure temp files during config apply (nginx, dnsmasq). Created by systemd `RuntimeDirectory=`, removed on stop. | Daemon (systemd unit) |
|
||||
|
||||
The `/etc/nginx/conf.d/vacuum-wall.conf` include file ensures that all domain-specific configurations in `sites-enabled/` are loaded by nginx without modifying the main `nginx.conf`. The SSL snippet keeps TLS settings consistent across all managed domains and allows global updates from a single location.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user