Update documentation for config/ migration, new install model, and firewall config API

Reflect config/ vs data/ split, install-time Jinja2 templating, auto-discovered
paths, ZeroSSL/ACME support, and the new firewall save-then-apply API in all
reference docs.
This commit is contained in:
2026-05-14 03:31:59 +00:00
parent 6106c1434d
commit dcb581a359
6 changed files with 166 additions and 90 deletions
+56 -28
View File
@@ -9,7 +9,7 @@ The following describes the path a request takes from an external client to a ba
1. An external client sends an HTTP request to `app.example.com`.
2. The request arrives at the Vacuum Wall host's WAN interface, assigned to the `external` firewalld zone. A firewall rule allows inbound traffic on port 443 (HTTPS).
3. nginx, listening on port 443, terminates the TLS connection using the domain's certificate.
4. nginx evaluates the `server_name` against the configured server blocks. The matching block is generated from the domain entry in `data/nginx/config.json`.
4. nginx evaluates the `server_name` against the configured server blocks. The matching block is generated from the domain entry in `config/nginx/config.json`.
5. The request is forwarded to the backend service (e.g., `192.168.2.50:8080`) via an `proxy_pass` directive.
6. The backend service processes the request and returns an HTTP response.
7. nginx adds security headers (`X-Content-Type-Options`, `X-Frame-Options`, HSTS, etc.) to the response.
@@ -17,7 +17,7 @@ The following describes the path a request takes from an external client to a ba
For HTTP requests (port 80), nginx returns a 301 redirect to the HTTPS equivalent before any proxying occurs.
### Management WebUI Access (e.g., `wall.lan`)
### Management WebUI Access (e.g., `<hostname>.local`)
1. A client sends an HTTPS request to the management domain.
2. nginx terminates TLS and checks for HTTP Basic Authentication credentials against the `.htpasswd` file.
@@ -36,42 +36,70 @@ External Client ──→ nginx (SSL termination) ──→ Flask WebUI (127.0.0
Flask WebUI ──→ lib/firewall.py ──→ sudo firewall-cmd ──→ firewalld / D-Bus ──→ nftables
Flask WebUI ──→ lib/nginx.py ──→ write local .conf files ──→ sudo cp to /etc/nginx/ ──→ sudo nginx -t && sudo nginx -s reload
Flask WebUI ──→ lib/dnsmasq.py ──→ render config ──→ sudo tee /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq
Flask WebUI ──→ lib/acme.py ──→ acme.sh (no sudo, runs as vacuum-wall user) ──→ Let's Encrypt ACME
Flask WebUI ──→ lib/wireguard.py ──→ render /home/wall/vacuum-wall/data/wireguard/wg0.conf ──→ sudo cp to /etc/wireguard/ ──→ sudo wg-quick up wg0
Flask WebUI ──→ lib/acme.py ──→ acme.sh (no sudo, runs as service user) ──→ ZeroSSL ACME
Flask WebUI ──→ lib/wireguard.py ──→ render $PROJECT_DIR/data/wireguard/wg0.conf ──→ sudo cp to /etc/wireguard/ ──→ sudo wg-quick up wg0
```
Each `lib/` module encapsulates command construction, privilege escalation (via sudo where needed), and error handling for its subsystem. The modules read declarative configuration from `data/`, render the appropriate system configuration files, and invoke the corresponding privileged operation. Note that `lib/acme.py` runs `acme.sh` without sudo — it executes as the unprivileged `vacuum-wall` user using webroot validation rather than standalone/TLS-ALPN modes that would require elevated privileges.
Each `lib/` module encapsulates command construction, privilege escalation (via sudo where needed), and error handling for its subsystem. The modules read declarative configuration from `config/` and runtime artifacts from `data/`, render the appropriate system configuration files, and invoke the corresponding privileged operation. Note that `lib/acme.py` runs `acme.sh` without sudo — it executes as the unprivileged service user using webroot validation rather than standalone/TLS-ALPN modes that would require elevated privileges.
The `lib/` modules auto-discover the project root at runtime via `Path(__file__).resolve().parent.parent`, so no hardcoded paths are needed in Python code.
## Install-Time Templating
System configuration files in `system/` are Jinja2 templates rendered by `install.sh` at install time:
- **`systemd/vacuum-wall.service`**, **`systemd/vacuum-wall-acme.service`** — `{{ USER_NAME }}`, `{{ PROJECT_DIR }}`, `{{ ACME_HOME }}` are substituted to produce the final systemd unit files installed to `/etc/systemd/system/`.
- **`sudoers.d/vacuum-wall`** — `{{ USER_NAME }}` is substituted to produce the sudoers whitelist.
- The timer file (`vacuum-wall-acme.timer`) contains no variable paths and is installed as-is.
Runtime templates (`system/nginx/*.conf`, `system/dnsmasq.conf`, `system/wireguard*.conf`) are rendered at runtime by `lib/` modules via Jinja2 with Python data.
## State Management
Vacuum Wall uses a declarative configuration model. The source of truth for each subsystem is a JSON file in the `data/` directory. The application renders these declarations into the format expected by the underlying system service.
Vacuum Wall uses a declarative configuration model. Persistent user-facing configuration lives in `config/<subsystem>/config.json`. Runtime artifacts and generated files live in `data/<subsystem>/`. The application renders these declarations into the format expected by the underlying system service.
| Subsystem | Declarative Config | Rendered Target | State Persistence |
|---|---|---|---|
| firewalld | `data/firewall/rules.json` | N/A (commands issued directly to firewalld via D-Bus) | firewalld manages its own persistent state in `/etc/firewalld/`. `rules.json` serves as a declarative backup and can be used to restore firewall rules. |
| dnsmasq | `data/dnsmasq/config.json` | `/etc/dnsmasq.d/vacuum-wall.conf` | The JSON file is the source of truth. The rendered `.conf` file is overwritten on each apply. |
| nginx | `data/nginx/config.json` | `data/nginx/sites-enabled/<domain>.conf` + `/etc/nginx/conf.d/vacuum-wall.conf` | All proxy and management domain definitions are derived from the JSON config. Generated `.conf` files are overwritten on each apply. |
| WireGuard | `data/wireguard/config.json` | `/etc/wireguard/wg0.conf` | The JSON file defines the interface and all peers. The rendered WireGuard config is overwritten on each apply. |
| ACME | `~/.acme.sh/` (managed by acme.sh) | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
| Subsystem | Declarative Config | Runtime Data | Rendered Target | State Persistence |
|---|---|---|---|---|
| firewalld | N/A (firewalld manages own state) | `data/firewall/rules.json` | N/A (commands issued directly to firewalld via D-Bus) | firewalld manages its own persistent state in `/etc/firewalld/`. `rules.json` serves as an automated backup snapshot. |
| dnsmasq | `config/dnsmasq/config.json` | `data/dnsmasq/fragments/` | `/etc/dnsmasq.d/vacuum-wall.conf` | The JSON file is the source of truth. The rendered `.conf` file is overwritten on each apply. |
| nginx | `config/nginx/config.json` | `data/nginx/.htpasswd`, `data/nginx/sites-enabled/` | `data/nginx/sites-enabled/<domain>.conf` + `/etc/nginx/conf.d/vacuum-wall.conf` | All proxy and management domain definitions are derived from the JSON config. Generated `.conf` files are overwritten on each apply. |
| WireGuard | `config/wireguard/config.json` | `data/wireguard/` | `/etc/wireguard/wg0.conf` | The JSON file defines the interface and all peers. The rendered WireGuard config is overwritten on each apply. |
| ACME | N/A (`~/.acme.sh/` managed by acme.sh) | `data/acme/` | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
## Data Directory Structure
## Directory Structure
### Config — Declarative Settings
Config files are persistent, user-editable JSON that defines the desired state for each subsystem:
```
data/
├── nginx/
│ ├── config.json # Proxy domain definitions, management domain, SSL settings
│ ├── .htpasswd # HTTP Basic Authentication credentials for management UI
│ └── sites-enabled/ # Generated nginx server block .conf files (one per domain)
config/
├── dnsmasq/
── config.json # DHCP ranges, static leases, DNS forwarding, custom records
│ └── fragments/ # User-defined dnsmasq config fragments (appended verbatim)
├── firewall/
│ └── rules.json # Declarative firewall rule state backup
── config.json # DHCP ranges, static leases, DNS forwarding, custom records
├── nginx/
│ └── config.json # Proxy domain definitions, management domain, SSL settings
└── wireguard/
└── config.json # WireGuard interface and peer configuration
```
The `data/` directory resides within the `vacuum-wall` user's project directory (`/home/wall/vacuum-wall/data/`). The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to this directory, while keeping the rest of the filesystem read-only.
### Data — Runtime Artifacts
The `data/` directory holds generated files, credentials, and subsystem artifacts:
```
data/
├── nginx/
│ ├── .htpasswd # HTTP Basic Authentication credentials for management UI
│ └── sites-enabled/ # Generated nginx server block .conf files (one per domain)
├── dnsmasq/
│ └── fragments/ # User-defined dnsmasq config fragments (appended verbatim)
├── firewall/
│ └── rules.json # Auto-generated firewall rule state backup
├── acme/ # ACME certificate files (acme.sh home)
└── wireguard/ # WireGuard runtime artifacts
```
Both `config/` and `data/` reside within the project directory (`$PROJECT_DIR/`). The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to both directories, while keeping the rest of the filesystem read-only. The `PROJECT_DIR` value is templated into the service unit at install time.
## File System Layout
@@ -81,9 +109,9 @@ The following file system locations are used for integration with system service
|---|---|---|
| `/etc/nginx/conf.d/vacuum-wall.conf` | Include directive that pulls in `data/nginx/sites-enabled/*.conf`. | Vacuum Wall (lib/nginx.py) |
| `/etc/nginx/snippets/vacuum-wall-ssl.conf` | Shared SSL configuration snippet (protocols, ciphers, DH parameters, OCSP). Included by all HTTPS server blocks. | Vacuum Wall (lib/nginx.py) |
| `/etc/dnsmasq.d/vacuum-wall.conf` | Generated dnsmasq configuration file. Written from `data/dnsmasq/config.json`. | Vacuum Wall (lib/dnsmasq.py) |
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `data/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
| `/etc/sudoers.d/vacuum-wall` | Sudo whitelist for the `vacuum-wall` user. Defines all permitted privilege escalations. | Install script (manual edits not required) |
| `/etc/dnsmasq.d/vacuum-wall.conf` | Generated dnsmasq configuration file. Written from `config/dnsmasq/config.json`. | Vacuum Wall (lib/dnsmasq.py) |
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `config/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
| `/etc/sudoers.d/vacuum-wall` | Sudo whitelist for the configured system user. Defines all permitted privilege escalations. | Install script (rendered from Jinja2 template) |
The `/etc/nginx/conf.d/vacuum-wall.conf` include file ensures that all domain-specific configurations in `sites-enabled/` are loaded by nginx without modifying the main `nginx.conf`. The SSL snippet keeps TLS settings consistent across all managed domains and allows global updates from a single location.
@@ -106,4 +134,4 @@ Additional zones can be created for specialized network segments:
- **Guest zone**: For visitor Wi-Fi or untrusted devices. Access is limited to outbound Internet traffic only, with no access to `internal` or `vpn` zones.
- **IoT zone**: For devices requiring restricted outbound access (e.g., blocking telemetry domains).
Each custom zone can define its own source rules, port forwardings, and inter-zone traffic policies. The Flask WebUI provides interfaces to create, modify, and assign interfaces to zones at runtime.
Each custom zone can define its own source rules, port forwardings, and inter-zone traffic policies. The Flask WebUI provides interfaces to create, modify, and assign interfaces to zones at runtime.