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:
+20
-27
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user