Refactor nginx to path-based domain model with config migration

Replace the legacy top-level management key with a unified paths-based
model. Each domain now contains a paths map where each entry defines its
own backend, auth, headers, and flags (is_management, is_websocket).

- Add _migrate_config() to auto-migrate legacy formats on first load
- Remove set_management_proxy() and POST_NGINX_MANAGEMENT endpoint
- Update server_block.conf template to iterate paths with per-location auth
- Update daemon handler, API blueprint, state collector, and install script
- Add server config generation tests for paths, WebSocket, auth inheritance
- Update frontend proxy page to display per-path rows with flags
This commit is contained in:
2026-06-27 23:34:06 +00:00
parent 8feb56faf6
commit 835326311b
14 changed files with 851 additions and 558 deletions
+20 -27
View File
@@ -715,13 +715,15 @@ Write the global nginx SSL snippet configuration.
GET /api/proxy/domains
```
Return all configured proxy domains.
Return all configured proxy domains. The response is flattened by path — each path within a domain produces a separate entry.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of domain configuration objects |
| `data` | `[object, ...]` | Array of path-level domain configuration objects |
Each entry contains `domain` (string), `path` (string), `backend` (object with `host`, `port`, `proto`), `online` (boolean), `force_ssl` (boolean), and optionally `is_management` or `is_websocket` flags.
---
@@ -731,9 +733,18 @@ Return all configured proxy domains.
POST /api/proxy/domains
```
Add a new reverse proxy domain.
Add a new reverse proxy domain. Accepts two modes:
**Request Body:**
**Paths mode (preferred):**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | `string` | Yes | Domain name to proxy |
| `paths` | `object` | Yes | Path-to-config map. Each path entry must have a `backend` key with `host`, `port`, `proto`. |
| `cert` | `string` | No | Certificate type |
| `force_ssl` | `boolean` | No | HTTPS redirect flag (default `true`) |
**Legacy mode (backward compatible):**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
@@ -741,7 +752,7 @@ Add a new reverse proxy domain.
| `backend_host` | `string` | Yes | Backend server IP or hostname |
| `backend_port` | `number` | Yes | Backend server port |
| `backend_proto` | `string` | No | Backend protocol (`"http"` or `"https"`); defaults to `"http"` |
| `cert` | `string` | No | Certificate domain |
| `cert` | `string` | No | Certificate type |
| `extra_headers` | `object` | No | Extra proxy headers |
**Response (`data`):**
@@ -774,9 +785,9 @@ Returns HTTP `404` if the domain is not configured.
PUT /api/proxy/domains/<domain>
```
Update one or more fields of an existing domain entry. Only fields present in the body are modified.
Update one or more fields of an existing domain entry. Only fields present in the body are modified. Supports both domain-level keys (`paths`, `force_ssl`, `cert`, `auth`) and path-level shorthand (`backend`, `headers` for the root path).
**Request Body:** Any subset of (`backend_host`, `backend_port`, `backend_proto`, `cert`, `extra_headers`).
**Request Body:** Any subset of (`paths`, `backend`, `backend_host`, `backend_port`, `backend_proto`, `cert`, `extra_headers`, `force_ssl`, `auth`).
**Response (`data`):**
@@ -837,27 +848,9 @@ Run `nginx -t` against the generated configuration without reloading.
**Error (invalid):** HTTP `400` with standard `{"ok": false, "error": "<nginx output>"}` response.
### Management
### Management Proxy
#### Configure Management WebUI Proxy
```
POST /api/proxy/management
```
Configure the nginx proxy block for the management WebUI itself, including optional HTTP basic authentication.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | `string` | Yes | Management domain (e.g., `"myhost.local"`) |
| `flask_host` | `string` | No | Flask app bind host; defaults to `"127.0.0.1"` |
| `flask_port` | `number` | No | Flask app bind port; defaults to `9090` |
| `auth_user` | `string` | No | Username for basic auth |
| `auth_pass` | `string` | No | Password for basic auth |
**Response:** `data` is `null` on success.
>The legacy `POST /api/proxy/management` endpoint has been removed. The management WebUI proxy is now configured as a regular domain entry with `is_management: true` on the root path and `is_websocket: true` on the `/ws` path. Use the standard domain add/update endpoints to configure it.
---
+86 -36
View File
@@ -76,35 +76,64 @@ Additional dnsmasq directives can be appended verbatim by placing plain-text fil
**File**: `config/nginx/config.json`
This file defines reverse proxy domains, the management interface, and global SSL settings. The application renders it into per-domain server block files in `data/nginx/sites-enabled/` and into the shared SSL snippet at `/etc/nginx/snippets/vacuum-wall-ssl.conf`.
This file defines reverse proxy domains with path-based routing, and global SSL settings. The application renders it into per-domain server block files in `data/nginx/sites-enabled/` and into the shared SSL snippet at `/etc/nginx/snippets/vacuum-wall-ssl.conf`.
```json
{
"domains": {
"app.example.com": {
"backend": {
"host": "192.168.2.50",
"port": 8080,
"proto": "http"
},
"force_ssl": true,
"cert": "acme",
"headers": {
"X-Forwarded-Proto": "https",
"X-Real-IP": "$remote_addr"
"auth": {
"user": "admin",
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
},
"paths": {
"/": {
"backend": {
"host": "192.168.2.50",
"port": 8080,
"proto": "http"
},
"headers": {
"X-Forwarded-Proto": "https"
}
},
"/api": {
"backend": {
"host": "192.168.2.51",
"port": 3000,
"proto": "http"
},
"auth": null
}
}
}
},
"management": {
"domain": "vacuum-wall.local",
"backend": {
"host": "127.0.0.1",
"port": 9090,
"proto": "http"
},
"auth": {
"user": "admin",
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
"mgmt.example.com": {
"force_ssl": true,
"cert": "acme",
"paths": {
"/": {
"backend": {
"host": "127.0.0.1",
"port": 9090,
"proto": "http"
},
"is_management": true,
"auth": {
"user": "admin",
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
}
},
"/ws": {
"backend": {
"host": "127.0.0.1",
"port": 9091,
"proto": "http"
},
"is_websocket": true
}
}
}
},
"ssl": {
@@ -117,17 +146,40 @@ This file defines reverse proxy domains, the management interface, and global SS
### Domain Entries
The `domains` object maps domain names (keys) to proxy configurations. Each entry produces a separate nginx `server` block.
The `domains` object maps domain names (keys) to proxy configurations. Each entry produces a separate nginx `server` block. All routing is path-based — a domain can proxy multiple paths to different backends.
| Field | Type | Required | Description |
|---|---|---|---|
| `backend` | object | Yes | The upstream service that receives proxied traffic. |
| `paths` | object | Yes | Path-to-config map. Each key is a URL path (e.g., `"/"`, `"/api"`). No catch-all unless `"/"` is explicitly defined. |
| `force_ssl` | boolean | No | Enable HTTPS redirect. HTTP requests to this domain receive a 301 redirect to HTTPS. Default: `true`. |
| `cert` | string | No | Certificate provisioning method. One of: `"acme"`, `"file"`, or `"selfsigned"`. Omit for domains that don't need a dedicated cert. |
| `auth` | object | No | Domain-level HTTP basic auth configuration (`{ user, htppasswd }`). Applies to all paths unless overridden at the path level. |
### Path Entries
Each entry under `paths` defines a location block and its proxy backend.
| Field | Type | Required | Description |
|---|---|---|---|
| `backend` | object | Yes | The upstream service for this path. |
| `backend.host` | string | Yes | IP address or hostname of the backend service. |
| `backend.port` | integer | Yes | Port the backend service is listening on. |
| `backend.proto` | string | No | Protocol for the backend connection: `http` or `https`. Default: `http`. |
| `force_ssl` | boolean | No | Enable HTTPS redirect. HTTP requests to this domain receive a 301 redirect to HTTPS. Default: `true`. |
| `headers` | object | No | Custom headers to set on proxied requests. Supports nginx variable interpolation (e.g., `$remote_addr`). |
| `cert` | string | No | Certificate provisioning method. One of: `"acme"`, `"file"`, or `"selfsigned"`. Omit for domains that don't need a dedicated cert. |
| `backend.proto` | string | No | Protocol: `http` or `https`. Default: `http`. |
| `headers` | object | No | Custom proxy headers (key-value pairs). Supports nginx variable interpolation. |
| `auth` | object \| null | No | Path-level auth override. `{ user, htppasswd }` replaces domain-level auth. `null` disables auth for this path. |
| `is_management` | boolean | No | Marks this path as the Vacuum Wall WebUI backend. Suppresses security headers (X-Frame-Options, etc.) so the SPA works correctly. |
| `is_websocket` | boolean | No | Marks this path as a WebSocket pass-through. Disables auth, sets Upgrade/Connection headers, uses extended timeouts. |
### Auth Inheritance Rules
- Domain-level `auth` applies to all paths unless overridden.
- Path-level `auth: null` means "no auth" for that path.
- Path-level `auth: { ... }` overrides domain-level for that path.
- No other domain-level settings inherit — `headers` is path-only.
### Path ordering
Nginx evaluates `location` blocks by specificity: more specific prefixes (e.g., `/api`) always match before `/` by nginx's own priority rules. The order of keys in the `paths` dict does not affect routing behavior.
### Certificate Types
@@ -141,22 +193,20 @@ The `cert` field is a string that selects the provisioning method:
### Management Domain
The `management` block configures the Vacuum Wall admin interface itself. It follows the same structure as a domain entry but can include an `auth` block for HTTP Basic Authentication.
The Vacuum Wall admin interface is configured as a regular domain entry under `domains`, with `is_management: true` on the path pointing to the Flask app. A second path (`/ws`) with `is_websocket: true` provides WebSocket pass-through for real-time state updates. This replaces the legacy `management` top-level key.
| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | Yes | The hostname used to access the management WebUI (e.g., `<hostname>.local`). |
| `backend` | object | Yes | Points to the Flask app at `127.0.0.1:9090`. |
| `auth` | object | No | HTTP Basic Authentication configuration. Only created if `auth_user` is provided when setting the management proxy. |
| `auth.user` | string | Yes | Username for the `.htpasswd` file. |
| `auth.htpasswd` | string | Yes | Full path to the `.htpasswd` file containing the username and hashed password. |
The application can create the `.htpasswd` file programmatically via `write_htpasswd()` (using passlib's `apache_passwd` with Apache-Round-12, falling back to SHA-256 crypt). Manual creation is also possible:
The application can create the `.htpasswd` file programmatically via `write_htpasswd()` (using passlib's SHA-256 crypt). Manual creation is also possible:
```bash
htpasswd -bc data/nginx/.htpasswd admin yourpassword
```
### Backward Compatibility
Config files using the legacy format are auto-migrated on first load:
- Domain entries with a top-level `backend` key are wrapped into `paths["/"]`.
- A legacy `management` top-level key is migrated into `domains[management.domain]` with `is_management` on the root path and a `/ws` WebSocket path.
### Global SSL Settings
The `ssl` block defines TLS parameters applied to all HTTPS server blocks via the shared snippet.