3.9 KiB
Vacuum Wall — Agent Instructions
What This Is
SSL proxy / firewall appliance. Python 3 Flask WebUI behind nginx reverse proxy.
Deploys on Debian 13 (trixie). Install dir: /opt/vacuum-wall.
Architecture
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ lib/*.py ──→ sudo <cmd> ──→ system service
webui/server.py— Flask app entry point. Only file that creates theapp.webui/api/*.py— Flask blueprints, one per subsystem. Routes prefix/api/<subsystem>/.lib/*.py— Backend modules. Wrap system commands viasubprocess.run(["sudo", ...]).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/(service 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/ and lib/ are intentionally empty — no sys.path boilerplate needed.
Deployment
install.sh deploys to /opt/vacuum-wall by rsyncing the repo. The vacuum-wall system user has HOME=/opt/vacuum-wall but no actual home directory (--no-create-home).
All Python modules use Path(__file__).resolve().parent.parent for PROJECT_DIR — no hardcoded paths. ACME certs live at PROJECT_DIR/data/acme/.
Local Dev
.venv/bin/python webui/server.py # binds 127.0.0.1:9090
In production the systemd unit runs as the vacuum-wall system user (NoNewPrivileges, ProtectSystem=strict, loopback-only networking).
Blueprint ↔ lib Mapping (Naming Is Not 1:1)
| 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 |
Privileged Operations
lib/ modules call sudo for everything that touches system services. Whitelist is system/sudoers.d/vacuum-wall.
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 lib/ code.
API Response Contract
- Success:
{"ok": true, "data": <value>}— helper_ok(data) - Error:
{"ok": false, "error": "msg"}— helper_error(msg, code=400) - HTTP codes:
400bad request,404not found,500internal failure - Full spec:
docs/api.md
Page Routes vs API
server.py serves HTML pages with Jinja templates. All data is wrapped in _safely(fn, default) so page routes never 500 — they render with fallback values instead.
Deploy
install.sh is the single deploy script. Run as root, requires MGMT_DOMAIN, MGMT_PASS, ACME_EMAIL env vars.
Lint and Tests
Linter / formatter: Ruff (ruff check + ruff format). Config in pyproject.toml under [tool.ruff].
Tests: pytest in tests/. Run with python -m pytest. Tests mock out subprocess calls (firewalld, acme.sh, WireGuard, nginx, dnsmasq) — 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 (154 tests)
Install dev tooling with pip install -e ".[dev]".
Docs
docs/ contains the authoritative reference. docs/architecture.md covers request flow, zone model, and data directory layout in detail.