mteehan ac52918df5 firewall: interface-coverage apply guard, target drift, non-destructive DHCP sync
Post-DHCP-incident hardening per HARDEN.md.

- apply guard: refuse (ConflictError, `force` overrides) when a
  network-managed interface would end up in no zone; absent
  `interfaces` key = hands-off, explicit `[]` = unassign-all
- surface `uncovered_interfaces` in firewall state (lo/wg* filtered)
  + advisory in /api/status/pending; zones.js banner + interfaces-picker
  last-zone confirm
- target drift (Option A): absent or default-normalizing target is
  unmanaged: not diffed, never re-set by apply; create_zone runs
  --new-zone first and sets non-default targets only; importer omits
  the target key for default zones
- FirewallToDhcpSync keeps stale DHCP ranges and flags them instead of
  deleting; `dnsmasq` affected only on a real gateway mutation
- real pre-apply recovery snapshot in data/firewall/rules.json
  ({timestamp, default_zone, zones, config}); drop the empty post-apply
  skeleton
- daemon shutdown: bounded grace for in-flight tasks + suppressed
  teardown exception noise on SIGTERM
- also carries the firewall service-descriptions feature
  (get_service_descriptions + service_descriptions state field + UI)
- tests + docs across firewall/status/state/sync/schema; ruff clean,
  867 passing
2026-08-28 23:38:21 +00:00

Vacuum Wall

A zone-based firewall appliance with a built-in SSL reverse proxy. Combines firewalld policy control, DHCP/DNS, WireGuard VPN, and automated certificate provisioning into a single device managed through one web UI.

Tech Stack

  • Debian 13 (trixie) target platform
  • Python 3.13+, Flask 3.x web UI
  • firewalld (nftables backend), dnsmasq, nginx, WireGuard
  • acme.sh for ACME certificates (ZeroSSL)

For Operators

Prerequisites

  • Clean Debian 13 system with root access
  • git installed
  • One public-facing NIC (external) and at least one LAN NIC (internal)
  • A DNS A record pointing to the appliance's public IP for the management domain
  • Minimum hardware: 1 CPU, 512 MB RAM, 4 GB disk

Install (Production)

MGMT_DOMAIN=wall.example.com \
MGMT_PASS="strongpassword" \
MGMT_USER="admin" \
ACME_EMAIL="admin@example.com" \
bash scripts/install.sh

Install (Development)

./scripts/install.sh --dev --mgmt-pass strongpassword --acme-email "admin@example.com"

--dev auto-detects the repo's file owner as the service user, skips the safety warning about running as a regular user, and keeps file ownership dev-friendly.

Flag Env Var Required Description
-- MGMT_DOMAIN No Public domain for the management WebUI (auto-detected as hostname.local)
--mgmt-pass MGMT_PASS Yes HTTP basic auth password for the WebUI
--mgmt-user MGMT_USER No WebUI username (defaults to admin)
--acme-email ACME_EMAIL Yes ACME registration email (ZeroSSL by default)
--user, -u USER_NAME No System user for service (default: vacuum-wall)
--path, -p INSTALL_DIR No Install directory (default: repo root)
--dev -- No Auto-detect repo owner as service user, skip safety warning
--mgmt-domain MGMT_DOMAIN No (alias for env var)
--wan-iface WAN_IFACE No WAN interface (auto-detected)
--lan-ifaces LAN_IFACES No LAN interfaces, comma-separated (auto-detected)

CLI flags take precedence over environment variables. Run ./scripts/install.sh --help for full usage.

After installation, access the WebUI at https://<MGMT_DOMAIN>. The initial certificate is self-signed — use the Certs tab to issue a real one once DNS is propagating.

Next Steps

  1. Assign interfaces to zones from the Interfaces tab
  2. Configure DHCP ranges for your LAN
  3. Add proxy domains with ACME certificates
  4. Set up WireGuard (optional)

See docs/deployment.md for the full guide, including troubleshooting.


For Developers

Local Development

git clone <repo-url> && cd vacuum-wall
bash scripts/update-vendor.sh
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

Start the WebUI locally (binds to 127.0.0.1:9090):

.venv/bin/python webui/server.py

Lint and Format

.venv/bin/ruff check lib/ webui/ tests/
.venv/bin/ruff format lib/ webui/ tests/

All lib/ modules share lib.common utilities (run, run_proc, load_json, save_json, deep_merge, ensure_dirs) and have full type hints and __all__ exports. API blueprints share _ok/_error from webui.api.common.

Tests

.venv/bin/python -m pytest tests/ -v

Tests mock all subprocess calls (firewalld, acme.sh, WireGuard, nginx, dnsmasq) — no system services required. 192 tests across 5 test modules.

Documentation MCP Server

This project uses docs-mcp-server for grounded, real-time documentation lookups during development.

pipx install docs-mcp-server
docs-mcp init

Then run with Claude Code or Opencode to activate it. It automatically checks live docs before answering technical questions — no manual doc search needed.

Architecture

Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ lib/*.py ──→ sudo <cmd> ──→ system service
Blueprint URL prefix Backend module
webui/api/firewall /api/firewall/ lib.firewall
webui/api/dhcp /api/dhcp/ lib.dnsmasq
webui/api/proxy /api/proxy/ lib.nginx
webui/api/certs /api/certs/ lib.acme
webui/api/wireguard /api/wireguard/ lib.wireguard

See docs/architecture.md for detailed request flow, zone model, and shared utility patterns.


Documentation

S
Description
No description provided
Readme 2.8 MiB
Languages
Python 73.1%
JavaScript 24%
Shell 1.6%
CSS 1.3%