Files

18 KiB

Vacuum Wall

What is Vacuum Wall?

Vacuum Wall is a zone-based firewall appliance with a built-in SSL reverse proxy, providing a unified platform for network security and traffic management. It combines firewalld policy control, DHCP/DNS services, systemd-networkd for static IP management, WireGuard VPN tunnels, and automated certificate provisioning into a single device. A single web UI controls everything, making enterprise-grade network infrastructure manageable from one place.

Architecture Overview

Vacuum Wall is built around six integrated subsystems managed through a two-layer architecture: a non-privileged Flask web UI and a privileged background daemon (vacuum-walld). The web UI communicates with the daemon via a Unix socket; the daemon also streams real-time state over a local WebSocket (127.0.0.1:9091) — a full snapshot on connect, then per-subsystem versions (structural) and tick (volatile-only) deltas — so the UI auto-refreshes without HTTP polling. The daemon handles all privileged operations (sudo) for the subsystems: the traffic plane uses firewalld with its nftables backend (zone-based policies, source NAT, destination NAT); the DNS/DHCP plane serves private subnets via dnsmasq; the network plane uses systemd-networkd for static IP management; the proxy plane runs nginx with automatic ACME certificates through acme.sh; the VPN plane uses WireGuard (wg-quick) for encrypted tunnel management; and the authentication subsystem manages users, passkeys, and JWT sessions. Certificate management is tracked as a standalone state subsystem with its own API. In total, lib/state.py tracks 7 state subsystems. All subsystems are configured and monitored through the Flask web UI, which is itself proxied through nginx (TLS termination only — management authentication is a Flask-layer JWT, not nginx basic auth; individual proxy domains may optionally configure their own basic auth).

Subsystems

Firewall

The firewall uses firewalld's zone model for traffic control. Network interfaces are assigned to zones such as external, internal, and trusted, plus a per-access-class vpn-<class> zone for each WireGuard access class (managed by the WireGuard sync). Zones carry an optional per-zone target (accept/drop/reject), and rules express fine-grained policies via services, port rules, and rich rules. Source NAT (masquerade) enables RFC 1918 networks to reach the internet through the external interface. Destination NAT rules provide port forwarding, exposing internal services to external networks on configurable ports.

Interface-coverage invariant. Every network-managed interface (lo/wg* excluded) must be covered by a zone in the firewall config or declared in the top-level unmanaged list. The invariant is enforced at save time (400) and at apply time (409; {"force": true} overrides); live drift is advisory only and surfaced as uncovered_interfaces in state.

Pending-changes model. Edits saved to a config are not applied until the operator applies them. Each subsystem exposes pending_changes plus a pending_diff of the changed fields, aggregated at GET /api/status/pending. POST /api/status/apply-all applies pending changes in dependency order (networkd → firewall → wireguard → dnsmasq → nginx); POST /api/status/cancel-all reverts all pending edits to the last-applied config.

DHCP/DNS

dnsmasq serves as both the DHCP server and local DNS resolver. It is configured to serve address pools on specified LAN interfaces, with support for dynamic allocation ranges and static MAC-based reservations. Custom DNS records can be defined for local name resolution, and upstream DNS forwarding passes external queries to configurable resolvers.

SSL Proxy

The nginx reverse proxy handles HTTPS termination for user-defined domains, with certificates automatically provisioned and renewed via acme.sh and the configured ACME provider (the CA is config-driven; the code default is Let's Encrypt). The configuration is a three-part model: named backends, domains that reference them, and a global ssl settings block. Each domain's paths resolve against its named backend's path table, and a builtin webui backend serves the management interface (Flask on 127.0.0.1:9090 plus the WebSocket on 127.0.0.1:9091). Backends are managed through the web UI (list, add, update, remove). Proxy domains may additionally gate paths with per-domain basic auth via a generated .htpasswd file — never on the management domain, which relies on the Flask-layer JWT. New proxy domains are added through the web UI, and the configuration is applied without manual intervention.

Network (systemd-networkd)

The networkd subsystem manages static IP configuration for network interfaces via systemd-networkd. It renders declarative JSON configuration into per-interface .network INI files (99-<name>.network), supporting static addresses, routes, DNS, DHCP clients, link settings, and all [Address], [Route], [DHCPv4], [DHCPv6], and [Link] section keys. When the handler applies an interface, it removes lower-priority conflicting .network files from the system directory. When the full apply runs, public DNS servers from networkd configs are auto-synced to dnsmasq's upstream resolvers. Helper endpoints can infer candidate DHCP ranges from static IPs and suggest firewalld zone assignments based on interface role.

WireGuard

WireGuard support provides server-side VPN tunnel management. Tunnels are organized into access classes: each class owns a wg-<class> interface, a vpn-<class> firewall zone, a dedicated subnet, listen port, and keypair, plus a lan_access flag controlling whether its peers can reach the LAN. Two classes exist by default (full, with LAN access, and internet, without). Classes are managed through CRUD endpoints (add, update, delete, reorder, generate keys). Peers are assigned to a class and added through the web UI, with the system generating client configuration files that can be downloaded and applied on remote devices. The dashboard displays active connections and transfer statistics for each peer, allowing operators to monitor tunnel health and usage.

Authentication

Authentication is a first-class subsystem. Users, per-subsystem read/rw permissions, Argon2id password hashes, and optional passkeys (WebAuthn/FIDO2) are stored in a SQLite database (data/auth.db), reached through an abstract database layer that never exposes raw SQL. Sessions use JWT access + refresh tokens: each user holds their own HS256 signing secret, and revoked tokens are blacklisted by jti. A builtin admin user is seeded at bootstrap. The subsystem exposes /api/auth/* endpoints and the login, users, and passkeys pages.

Certificates (ACME)

Certificate management is a standalone state subsystem with its own API (/api/certs/*). acme.sh issues and renews certificates for proxy domains against the configured CA provider; self-signed certificates can be generated for domains without an ACME account, and ACME accounts can be registered or deactivated. A systemd timer runs periodic renewals, and certificate state (issuance, expiry) is collected like any other subsystem.

Tech Stack

  • Debian 13 (trixie) target platform
  • Python 3.13+, Flask 3.x for web management
  • aiohttp (daemon server) + requests-unixsocket (Unix-socket client)
  • firewalld (nftables backend)
  • systemd-networkd (networkctl)
  • nginx
  • dnsmasq
  • WireGuard tools (wireguard-tools)
  • acme.sh for ACME certificate management (CA provider config-driven; code default Let's Encrypt)
  • SQLite (auth database)
  • PyJWT (JWT sessions), argon2-cffi (Argon2id password hashing), webauthn (passkeys), passlib (htpasswd only)
  • htm.js (vendored JS tagged-template HTML adapter)

Quick Start

To install Vacuum Wall on a Debian 13 system, run scripts/install.sh as root with required settings (CLI flags or environment variables):

# Production
./scripts/install.sh --mgmt-pass yourpassword

# Development (auto-detects your user)
./scripts/install.sh --dev --mgmt-pass yourpassword

After installation, access the management interface at https://<hostname>.local using the credentials you configured. The scripts/install.sh script auto-detects the system hostname, network interfaces, and provisions nginx, authentication, an initial self-signed certificate, and all services. Run ./scripts/install.sh --help for all options.

Project Structure

├── README.md                # Project overview
├── AGENTS.md                # Agent instructions
├── .gitignore
├── pyproject.toml           # Project metadata + dependencies
├── scripts/                 # Utility scripts
│   ├── install.sh           # Deployment script (renders Jinja2 templates)
│   ├── update-vendor.sh     # Download vendored libraries (acme.sh, htm)
│   ├── bootstrap_auth.py    # Auth DB bootstrap (creates the operator user)
│   └── restart-services.sh  # Restart installed system services
├── config/                  # Declarative JSON configuration (source of truth)
│   ├── firewall/            # Firewall zone & rule config
│   ├── dnsmasq/             # DHCP/DNS config
│   ├── network/             # systemd-networkd per-interface config
│   ├── nginx/               # Proxy backend, domain & SSL config
│   ├── wireguard/           # VPN access-class, interface & peer config
│   ├── acme/                # ACME account settings (email, CA provider)
│   └── auth/                # Authentication settings (JWT, WebAuthn)
├── data/                    # Runtime artifacts & generated files
│   ├── auth.db              # SQLite auth database (users, passkeys)
│   ├── certs/               # Management-domain TLS keypair
│   ├── daemon.sock          # Daemon Unix socket
│   ├── nginx/sites-enabled/ # Generated server blocks
│   ├── nginx/.htpasswd      # Basic-auth entries for proxy domains
│   ├── dnsmasq/fragments/   # User config fragments
│   ├── acme/                # acme.sh home: certs, account, webroot (www/)
│   ├── firewall/rules.json  # Pre-apply recovery snapshot
│   ├── networkd/            # Generated 99-<name>.network files
│   ├── wireguard/           # Generated WireGuard configs
│   └── logs/                # Application logs
├── daemon/                  # Privileged background daemon
│   ├── server.py            # aiohttp server: endpoint registry (daemon/iface.py), batch routing, WebSocket broadcast (snapshot/versions/tick), state refresh, per-subsystem polling
│   ├── client.py            # Sync HTTP client over Unix socket
│   ├── iface.py             # Single source of truth for daemon API endpoints
│   ├── __main__.py          # Module entry point (python -m daemon.server)
│   ├── handlers/            # Privileged operation handlers (all sudo calls)
│   │   ├── firewall.py      # Zone/rich-rule CRUD + apply
│   │   ├── dnsmasq.py       # DHCP/DNS config + apply
│   │   ├── nginx.py         # Proxy domain/backend + SSL apply
│   │   ├── network.py       # networkd handler (generate + apply)
│   │   ├── wireguard.py     # Access-class/peer CRUD + tunnel control
│   │   ├── acme.py          # Certificate issue/renew/self-signed, account
│   │   ├── auth.py          # User/passkey management
│   │   ├── logs.py          # Log streaming
│   │   ├── status.py        # Pending/apply-all/cancel-all
│   │   ├── system.py        # System info & metrics
│   │   └── common.py        # Shared handler helpers (sync emit + refresh)
│   └── collectors/          # Read-only per-subsystem state collectors
│       ├── firewall.py      # firewall collector
│       ├── dnsmasq.py       # dnsmasq collector
│       ├── networkd.py      # networkd collector
│       ├── nginx.py         # nginx collector
│       ├── wireguard.py     # wireguard collector
│       ├── acme.py          # acme collector
│       └── system.py        # system collector
├── system/                  # System file templates (mostly Jinja2)
│   ├── systemd/             # Service and timer unit files
│   │   ├── vacuum-wall.service        # Web UI service (rendered at install)
│   │   ├── vacuum-wall-acme.service   # Certificate renewal (rendered at install)
│   │   ├── vacuum-wall-acme.timer     # Renewal schedule
│   │   └── vacuum-walld.service       # Privileged daemon (rendered at install)
│   ├── sudoers.d/           # Sudo whitelist (rendered at install)
│   ├── tmpfiles.d/          # tmpfiles.d spec (installed verbatim, not Jinja)
│   ├── nginx/               # Nginx config templates (rendered at runtime)
│   ├── dnsmasq.conf         # Dnsmasq template (rendered at runtime)
│   ├── wireguard.conf       # WireGuard server template (rendered at runtime)
│   ├── wireguard-client.conf# WireGuard client template (rendered at runtime)
│   ├── acme-deploy.py       # ACME deploy hook (installed verbatim, not Jinja)
│   └── acme-deploy.sh       # ACME deploy wrapper (installed verbatim, not Jinja)
├── lib/                     # Subsystem abstraction layer
│   ├── common.py            # Shared utilities (run, run_proc, load_json, save_json, deep_merge, ensure_dirs, get_interface_ip, config_hash, stamp_applied, strip_apply_meta, compute_pending, deep_diff, revert_to_applied, validate_interface_name)
│   ├── logging.py           # Logging setup
│   ├── firewall.py          # firewalld bindings
│   ├── network.py           # systemd-networkd rendering & parsing
│   ├── dnsmasq.py           # DHCP/DNS configuration
│   ├── nginx.py             # Reverse proxy configuration (backends model)
│   ├── state.py             # In-memory state store (per-subsystem data, version counters, two-layer versions/tick diff, poll intervals, volatile registration); collectors live in daemon/collectors/
│   ├── sync.py              # Cross-subsystem event bus
│   ├── acme.py              # Certificate management (ACME helpers)
│   ├── wireguard.py         # VPN tunnel and peer management
│   ├── system_import.py     # Startup reconciler (imports live system configs into JSON)
│   ├── bootstrap.py         # Daemon-startup filesystem bootstrap
│   ├── schema.py            # TypedDict state schemas
│   ├── auth.py              # JWT access+refresh tokens, per-user HS256 secrets, jti blacklist
│   ├── auth_users.py        # Multi-user management, per-subsystem read/rw permissions, builtin admin
│   ├── password.py          # Argon2id password hashing
│   ├── webauthn.py          # Passkey (FIDO2/WebAuthn) support
│   ├── db.py                # Abstract database layer (opaque query IDs)
│   └── db_sqlite.py         # SQLite backend (data/auth.db)
├── webui/                   # Flask web application
│   ├── server.py            # Application entry point
│   ├── api/                 # REST API route modules (blueprints)
│   │   ├── common.py        # Shared API response helpers (_ok, _error)
│   │   ├── firewall.py      # Firewall API
│   │   ├── dhcp.py          # DHCP/DNS API
│   │   ├── proxy.py         # Nginx proxy API (domains + backends)
│   │   ├── certs.py         # Certificate API
│   │   ├── wireguard.py     # WireGuard API
│   │   ├── network.py       # Networkd API
│   │   ├── logs.py          # Logs API
│   │   ├── auth.py          # Authentication API
│   │   └── status.py        # Status API (pending/apply-all/cancel-all)
│   └── static/              # SPA (index.html, app.js, style.css)
│       ├── hoover/          # Hoover SPA framework (VDOM, reactivity, router, components)
│       │   ├── index.js     # Barrel export of all public APIs
│       │   ├── reactivity.js
│       │   ├── vdom.js
│       │   ├── render.js
│       │   ├── component.js
│       │   ├── router.js
│       │   ├── websocket.js
│       │   ├── api.js
│       │   ├── helpers.js
│       │   ├── html.js      # htm.js tag adapter
│       │   ├── model.js     # Reactive model store
│       │   ├── auth_model.js# Auth session model
│       │   ├── dirty.js     # Dirty-state tracking
│       │   ├── schema.js    # Schema validation helpers
│       │   └── components/  # applyconfirm, auth, data, layout, modal, qr, toast
│       └── pages/           # 15 page modules (each defines a route via definePage):
│                            # dashboard, zones, rules, nat, interfaces, dhcp,
│                            # proxy, backends, certs, wireguard, logs, login,
│                            # users, passkeys, notfound
├── vendor/                  # Vendored scripts and JS libraries
│   ├── acme.sh              # ACME certificate client
│   ├── htm.js               # JS tagged-template HTML adapter
│   └── qrcode-svg-1.1.0.js  # QR code generation (SVG)
├── tests/                   # Test suites
│   ├── test_*.py            # 28 Python modules (pytest; subprocess calls mocked)
│   └── test-*.js            # 9 JS test modules (hoover framework)
└── docs/                    # Documentation
    ├── overview.md          # This file
    ├── deployment.md
    ├── api.md
    ├── security.md
    ├── architecture.md
    ├── config.md
    ├── state-model.md       # State schema, versions/tick diff, pending-changes model
    └── hoover.md            # Hoover SPA framework

Documentation