feat: add ACME account management with validation pipeline

- 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
This commit is contained in:
2026-06-23 14:24:19 +00:00
parent 3a325504ec
commit 5025dfaf30
19 changed files with 2073 additions and 141 deletions
+22 -18
View File
@@ -9,10 +9,13 @@ Deploys on Debian 13 (trixie). Serves from repo root by default.
```
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ daemon/client.py (Unix socket) ──→ vacuum-walld (aiohttp, daemon.sock)
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 with `NOPASSWD sudo` whitelist (`/etc/sudoers.d/vacuum-walld`). Owns socket. Primary group is the WebUI user's primary group. Daemon user name is derived: `USER_NAME` + `d`.
@@ -37,7 +40,7 @@ vacuum-walld ──→ daemon/handlers/*.py ──→ sudo <cmd> ──→ syste
- `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/` (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.
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)
@@ -57,7 +60,7 @@ Conventions:
- Unix socket at `data/daemon.sock` (configurable via `VACUUM_WALLD_SOCKET` env var)
- WebSocket at `127.0.0.1:9091` (configurable via `VACUUM_WALLD_WS_PORT`) for real-time state change notifications
- Can also be started as `python -m daemon` or via the `vacuum-walld` console script
- Can be started as `python -m daemon.server` or via the `vacuum-walld` console script
## Environment Variables
@@ -85,17 +88,17 @@ Reload running Flask via SIGHUP (auto-reloads `webui.*` and `lib.*` modules, the
In production: systemd units run with `NoNewPrivileges`, `ProtectSystem=strict`, loopback-only networking.
## Blueprint ↔ lib Mapping
## Blueprint ↔ Handler ↔ lib Mapping
| 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` |
| `webui/api/network` | `/api/network/` | `lib.network` |
| `webui/api/logs` | `/api/logs/` | `lib.logging` |
| 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
@@ -108,10 +111,10 @@ Adding a new privileged command requires a sudoers entry **and** the `daemon/han
## API Response Contract
- Success: `{"ok": true, "data": <value>}` — helper `_ok(data)` from `webui.api.common`
- 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
- Success: `{"ok": true, "data": <value>}` — helper `_ok(data)` from `webui.api.common` (Flask) or `ok(data)` from `daemon.server` (aiohttp).
- Error: `{"ok": false, "error": "msg"}` — helper `_error(msg, code=400)` from `webui.api.common` or `error(msg, code)` from `daemon.server`.
- `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.
## Deploy
@@ -121,7 +124,7 @@ Adding a new privileged command requires a sudoers entry **and** the `daemon/han
**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.
**Tests:** pytest in `tests/`. Tests mock out subprocess calls — no system services required.
```bash
.venv/bin/ruff check lib/ webui/ tests/ # lint
@@ -142,6 +145,7 @@ Install dev tooling with `pip install -e ".[dev]"`.
| `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