refactor: introduce two-user daemon architecture with socket-based communication
- Add daemon/ module with aiohttp server, sync client, and handler registry - Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard) - Add system/acme-deploy.py, vacuum-walld sudoers and systemd service - Update API routes to use daemon client instead of lib/ directly - Update lib/, tests/, and webui/ for new architecture - Update docs and deployment scripts
This commit is contained in:
@@ -9,14 +9,26 @@ Deploys on Debian 13 (trixie). Serves from repo root by default.
|
||||
|
||||
```
|
||||
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
|
||||
Flask ──→ lib/*.py ──→ sudo <cmd> ──→ system service
|
||||
Flask ──→ daemon/client.py (Unix socket) ──→ vacuum-walld (aiohttp, daemon.sock)
|
||||
vacuum-walld ──→ daemon/handlers/*.py ──→ sudo <cmd> ──→ system service
|
||||
```
|
||||
|
||||
### Two-User Model with Shared Group
|
||||
|
||||
- **`vacuum-walld`** (daemon user): runs the privileged background daemon with `NOPASSWD sudo` whitelist (`/etc/sudoers.d/vacuum-walld`). Owns project directory and socket.
|
||||
- **`vacuum-wall`** (WebUI user): runs the Flask process with **zero sudo** access. Communicates with the daemon via Unix socket.
|
||||
- **`vacuum-wall`** (shared group): both users belong to this group. Socket is `vacuum-walld:vacuum-wall` with mode `0660`. Project dir is owned by `vacuum-walld:vacuum-wall` with group-read+execute.
|
||||
|
||||
### Code Layout
|
||||
|
||||
- `webui/server.py` — Flask app entry point. **Only** file that creates the `app`.
|
||||
- `webui/api/*.py` — Flask blueprints, one per subsystem. Routes prefix `/api/<subsystem>/`.
|
||||
- `webui/api/*.py` — Flask blueprints, one per subsystem. Routes prefix `/api/<subsystem>/`. All call `daemon.client` instead of `lib/` directly.
|
||||
- `webui/api/common.py` — Shared `_ok()` / `_error()` response helpers used by all blueprints.
|
||||
- `daemon/server.py` — aiohttp server, cache engine, batch routing, handler registry.
|
||||
- `daemon/client.py` — Sync HTTP client over Unix socket using `requests_unixsocket.Session`.
|
||||
- `daemon/handlers/*.py` — Privileged operation handlers (all `sudo` calls live here).
|
||||
- `lib/common.py` — Shared utilities: `run()`, `run_proc()`, `load_json()`, `save_json()`, `deep_merge()`, `ensure_dirs()`. All `lib/` modules use these instead of defining local helpers.
|
||||
- `lib/*.py` — Backend modules. All have full type hints and `__all__` exports.
|
||||
- `lib/*.py` — Backend modules (parsing, config, shared logic). All have full type hints and `__all__` exports. No sudo calls — privilege escalation is handled by `daemon/handlers/*.py`.
|
||||
- `data/` — Runtime artifacts (generated .confs, `.htpasswd`, ACME certs, firewall backup, dnsmasq fragments).
|
||||
- `config/<subsystem>/config.json` — Declarative JSON configs (source of truth). Generated `.conf` in `data/nginx/sites-enabled/`. Certs in `data/acme/`.
|
||||
- `system/` — System file templates. `systemd/` (service units installed to `/etc/systemd/system/`), `sudoers.d/`, `nginx/`.
|
||||
@@ -45,6 +57,8 @@ All Python modules use `Path(__file__).resolve().parent.parent` for `PROJECT_DIR
|
||||
|
||||
In production the systemd unit runs as the `vacuum-wall` system user (`NoNewPrivileges`, `ProtectSystem=strict`, loopback-only networking).
|
||||
|
||||
When `install.sh --dev` is used, the repo owner gets NOPASSWD sudo for system service commands (`nginx -t`, `nginx -s reload`, `firewall-cmd`, `wg`, `systemctl reload dnsmasq`, etc.). This allows invoking those commands directly in bash to inspect or test live system state during debugging, without relying on the mocked test suite.
|
||||
|
||||
## Blueprint ↔ lib Mapping (Naming Is Not 1:1)
|
||||
|
||||
| Blueprint | URL prefix | Backend module |
|
||||
@@ -57,12 +71,12 @@ In production the systemd unit runs as the `vacuum-wall` system user (`NoNewPriv
|
||||
|
||||
## Privileged Operations
|
||||
|
||||
`lib/` modules call `sudo` for everything that touches system services. Whitelist is `system/sudoers.d/vacuum-wall`.
|
||||
`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 `lib/` code.
|
||||
Adding a new privileged command requires a sudoers entry **and** the `daemon/handlers/` code.
|
||||
|
||||
## API Response Contract
|
||||
|
||||
@@ -70,8 +84,6 @@ Adding a new privileged command requires a sudoers entry **and** the `lib/` code
|
||||
- Error: `{"ok": false, "error": "msg"}` — helper `_error(msg, code=400)` from `webui.api.common`
|
||||
- `acme.issue()` / `acme.renew()` raise `RuntimeError` on failure — API layer wraps in try/except
|
||||
- HTTP codes: `400` bad request, `404` not found, `500` internal 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.
|
||||
@@ -89,14 +101,23 @@ Adding a new privileged command requires a sudoers entry **and** the `lib/` code
|
||||
```bash
|
||||
.venv/bin/ruff check lib/ webui/ tests/ # lint
|
||||
.venv/bin/ruff format lib/ webui/ tests/ # format
|
||||
.venv/bin/python -m pytest tests/ -v # test (192 tests)
|
||||
.venv/bin/python -m pytest tests/ -v # test (212 tests)
|
||||
```
|
||||
|
||||
Install dev tooling with `pip install -e ".[dev]"`.
|
||||
|
||||
## Docs
|
||||
|
||||
`docs/` contains the authoritative reference. `docs/architecture.md` covers request flow, zone model, data directory layout, and shared utility patterns in detail.
|
||||
`docs/` contains the authoritative reference for each subsystem.
|
||||
|
||||
| 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/overview.md` | Subsystem summaries, tech stack, complete project directory tree |
|
||||
|
||||
## Important Rules
|
||||
1. Ask, don't assume. If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements.
|
||||
|
||||
Reference in New Issue
Block a user