- Register, view, and deactivate ACME accounts via API and UI - 16-check validation framework for certificate issuance readiness - DNS resolution, port, nginx, and firewall pre-flight checks - External IP detection with NAT support and fallback providers - Account card and settings modal in certificates page - Guard certificate issuance behind account registration - Update modal CSS to overlay-based approach - 1000+ lines of tests for validation and account handlers
9.2 KiB
Vacuum Wall — Agent Instructions
What This Is
SSL proxy / firewall appliance. Python 3.13+ Flask SPA behind nginx reverse proxy. Deploys on Debian 13 (trixie). Serves from repo root by default.
Architecture
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ daemon/client.py (Unix socket) ──→ vacuum-walld (aiohttp, daemon.sock)
vacuum-walld ──→ daemon/handlers/*.py ──→ sudo <cmd> ──→ system service
Blueprints are thin proxies — they never call lib/ directly. All operations flow
through the daemon client over a Unix socket.
Two-User Model with Shared Group
vacuum-walld(daemon user): runs the privileged background daemon withNOPASSWD sudowhitelist (/etc/sudoers.d/vacuum-walld). Owns socket. Primary group is the WebUI user's primary group. Daemon user name is derived:USER_NAME+d.- WebUI user (default: repo owner in
--devmode): runs the Flask process with zero sudo access. Communicates with the daemon via Unix socket. - Shared group: both users share the WebUI user's primary group. Socket is
vacuum-walld:<group>with mode0660.
Code Layout
webui/server.py— Flask app entry point. Only file that creates theapp. SPA catch-all rendersindex.htmlwith server-side__WS_URL_PLACEHOLDER__substitution (no Jinja).webui/api/*.py— Flask blueprints, one per subsystem. Routes prefix/api/<subsystem>/. All calldaemon.clientinstead oflib/directly.webui/api/common.py— Shared_ok()/_error()response helpers used by all blueprints.daemon/server.py— aiohttp server, route registry, batch routing, WebSocket broadcast, state refresh.daemon/client.py— Sync HTTP client over Unix socket usingrequests_unixsocket.Session.daemon/iface.py— Single source of truth for all daemon API endpoints. Every endpoint is a frozen(method, path)tuple. Renaming an endpoint here auto-updates both server registry and client calls. All blueprints and handlers import from here.daemon/handlers/*.py— Privileged operation handlers (allsudocalls live here).lib/state.py— In-memory state store with per-subsystem collectors. Populated at daemon startup, refreshed on request. Backs WebSocket versioning/broadcast.lib/common.py— Shared utilities:run(),run_proc(),load_json(),save_json(),deep_merge(),ensure_dirs(),validate_interface_name(). Alllib/modules use these instead of defining local helpers.lib/logging.py— Logging setup used by both webui and daemon.lib/*.py— Backend modules (parsing, config, shared logic). Full type hints and__all__exports. No sudo calls.vendor/— Vendored scripts and JS libraries (acme.sh,htmx,json-enc).data/— Runtime artifacts (generated .confs,.htpasswd, ACME certs, firewall backup, dnsmasq fragments).config/<subsystem>/config.json— Declarative JSON configs (source of truth). Generated.confindata/nginx/sites-enabled/. Certs indata/acme/.system/— System file templates.systemd/(units installed to/etc/systemd/system/),sudoers.d/,nginx/.
Project uses .venv. Install deps with pip install -e . (from pyproject.toml). __init__.py files in webui/, lib/, and daemon/ are intentionally empty.
Frontend (hoover)
Custom reactive SPA framework at webui/static/hoover/. See docs/hoover.md for full API reference.
Conventions:
- All imports from
/static/hoover/index.js(barrel export of reactivity, VDOM, router, API, components). - Pages in
webui/static/pages/exportdefinePage({ init, subscribe, load, render })as default. - Bootstrap:
webui/static/app.jsmounts two render roots (#sidebar,#main), thenconnect()for WS. h()builds VNodes;#comp+hComp()for component lifecycle;keyfor keyed diff.- Events use
on:prefix (on:click,on:submit).classprop accepts object. - State always has
loading,refreshing,errorplus data.load()receives(state, abortController, entry). openModal+formModalfor dialogs;apiSubmit()for form submission.ToastContainer()in main root.- No build step — ES modules served raw. Assets versioned via
?v=Nquery string.
Daemon Endpoints
- Unix socket at
data/daemon.sock(configurable viaVACUUM_WALLD_SOCKETenv var) - WebSocket at
127.0.0.1:9091(configurable viaVACUUM_WALLD_WS_PORT) for real-time state change notifications - Can be started as
python -m daemon.serveror via thevacuum-walldconsole script
Environment Variables
VACUUM_WALL_DEV— dev mode flag; when set, disables aggressive static asset cachingVACUUM_WALLD_SOCKET— override daemon socket pathVACUUM_WALLD_WS_PORT— override WebSocket port (default9091)
Deployment
install.sh installs only system components and configures them; the project serves from the repo root by default. Options via env vars or CLI flags (CLI takes precedence). Set INSTALL_DIR or --path to override. Use --dev to auto-detect repo owner as service user; non-dev requires --user.
All Python modules use Path(__file__).resolve().parent.parent for PROJECT_DIR — no hardcoded paths. ACME certs at PROJECT_DIR/data/acme/.
Service Start Order
firewalld → avahi-daemon → dnsmasq → vacuum-walld → vacuum-wall
Local Dev
.venv/bin/python webui/server.py # binds 127.0.0.1:9090
Reload running Flask via SIGHUP (auto-reloads webui.* and lib.* modules, then SIGTERM restart).
In production: systemd units run with NoNewPrivileges, ProtectSystem=strict, loopback-only networking.
Blueprint ↔ Handler ↔ lib Mapping
| Blueprint | URL Prefix | Handler Module | lib Module |
|---|---|---|---|
webui/api/firewall |
/api/firewall/ |
daemon/handlers/firewall |
lib.firewall |
webui/api/dhcp |
/api/dhcp/ |
daemon/handlers/dnsmasq |
lib.dnsmasq |
webui/api/proxy |
/api/proxy/ |
daemon/handlers/nginx |
lib.nginx |
webui/api/certs |
/api/certs/ |
daemon/handlers/acme |
lib.acme |
webui/api/wireguard |
/api/wireguard/ |
daemon/handlers/wireguard |
lib.wireguard |
webui/api/network |
/api/network/ |
daemon/handlers/network |
lib.network |
webui/api/logs |
/api/logs/ |
daemon/handlers/logs |
— |
Privileged Operations
daemon/handlers/*.py call sudo for everything that touches system services. Whitelist is system/sudoers.d/vacuum-walld.
acme.sh must never run as root — always as the service user via sudo -u.
Pattern for mutations: write JSON → render native config → sudo <cmd> to apply.
Adding a new privileged command requires a sudoers entry and the daemon/handlers/ code.
API Response Contract
- Success:
{"ok": true, "data": <value>}— helper_ok(data)fromwebui.api.common(Flask) orok(data)fromdaemon.server(aiohttp). - Error:
{"ok": false, "error": "msg"}— helper_error(msg, code=400)fromwebui.api.commonorerror(msg, code)fromdaemon.server. acme.issue()/acme.renew()raiseRuntimeErroron failure — API layer wraps in try/except.- HTTP codes:
400bad request,404not found,500internal failure.
Deploy
install.sh is the single deploy script. Run as root. Only MGMT_PASS is strictly required; MGMT_DOMAIN is auto-detected from hostname.
Lint and Tests
Linter / formatter: Ruff (ruff check + ruff format). Config in pyproject.toml under [tool.ruff].
Tests: pytest in tests/. Tests mock out subprocess calls — no system services required.
.venv/bin/ruff check lib/ webui/ tests/ # lint
.venv/bin/ruff format lib/ webui/ tests/ # format
.venv/bin/python -m pytest tests/ -v # test
Install dev tooling with pip install -e ".[dev]".
Docs
docs/ contains the authoritative reference for each subsystem. Before reasoning about any subsystem, read the relevant doc(s) below.
| Doc | Contents |
|---|---|
docs/architecture.md |
Request flow, subsystem communication, two-user model, zone model, state management |
docs/security.md |
Privilege model, sudo whitelist, systemd hardening, TLS config, zone trust levels |
docs/deployment.md |
Install script options, what install.sh does, post-install setup, troubleshooting |
docs/config.md |
JSON schema for each subsystem config (dnsmasq, nginx, wireguard, cert types) |
docs/api.md |
REST API endpoint reference, request/response contracts, route patterns |
docs/hoover.md |
Custom frontend framework API reference |
docs/overview.md |
Subsystem summaries, tech stack, complete project directory tree |
Important Rules
- Ask, don't assume. If something is unclear, ask before writing a single line.
- Simplest solution first. Always implement the simplest thing that could work.
- Don't touch unrelated code. If a file or function is not directly part of the current task, do not modify it.
- Flag uncertainty explicitly. If you are not confident about an approach or technical detail, say so.