- Fix WireGuard private key leak in API responses and config updates - Update systemd service to serve from repo root with adjusted sandbox - Add CLI flags, idempotency, and dev mode to install.sh - Extract common utilities to lib/common.py and webui/api/common.py - Migrate frontend to htmx for simpler, more maintainable UI - Update docs to reflect current architecture and deployment model - Vendor htmx dependencies per project requirements
12 KiB
Configuration Reference
This document describes the JSON configuration files used by Vacuum Wall to manage each subsystem. All persistent configuration is stored in the config/ directory as declarative JSON. Runtime artifacts and generated files live in data/. The application renders these declarations into the format expected by each underlying service.
DHCP/DNS Configuration
File: config/dnsmasq/config.json
This file defines all DHCP server settings and DNS resolution behavior for the dnsmasq service. The application renders it into /etc/dnsmasq.d/vacuum-wall.conf.
{
"dhcp": {
"ranges": [
{
"interface": "eth1",
"start": "192.168.2.100",
"end": "192.168.2.200",
"lease_time": "12h",
"gateway": "192.168.2.1",
"dns": "192.168.2.1"
}
],
"static_leases": [
{
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.2.50",
"hostname": "printer"
}
]
},
"dns": {
"upstreams": ["8.8.8.8", "1.1.1.1"],
"domain": "lan",
"custom_records": [
{
"name": "nas.lan",
"address": "192.168.2.10",
"hostname": "nas"
}
]
}
}
DHCP Fields
| Field | Type | Required | Description |
|---|---|---|---|
ranges |
array | Yes | One or more DHCP address pools. Each range defines a subnet from which addresses are leased. |
ranges[].interface |
string | Yes | Network interface on which to serve this DHCP range (e.g., eth1). |
ranges[].start |
string | Yes | First IP address in the pool. |
ranges[].end |
string | Yes | Last IP address in the pool. |
ranges[].lease_time |
string | No | DHCP lease duration. Accepts values like 12h, 1d, 30m. Default: 1h. |
ranges[].gateway |
string | No | Default gateway advertised to DHCP clients. Typically the router's LAN IP. |
ranges[].dns |
string | No | DNS server address advertised to DHCP clients. Typically the Vacuum Wall host's LAN IP. |
static_leases |
array | No | Fixed IP assignments tied to MAC addresses. Clients with matching MACs always receive the specified IP. |
static_leases[].mac |
string | Yes | MAC address of the client (colon-separated lowercase hex). |
static_leases[].ip |
string | Yes | The IP address to assign to this MAC. Must be outside the dynamic pool ranges. |
static_leases[].hostname |
string | No | Hostname to associate with the lease. Used for reverse DNS and mDNS. |
DNS Fields
| Field | Type | Required | Description |
|---|---|---|---|
upstreams |
array | Yes | Upstream DNS servers to forward unresolved queries to. Supports IPv4 and IPv6 addresses. |
domain |
string | Yes | Local domain suffix. Hostnames without a FQDN are resolved within this domain (e.g., printer becomes printer.lan). |
custom_records |
array | No | Static DNS A records for internal services and devices. |
custom_records[].name |
string | Yes | Fully qualified domain name (e.g., nas.lan). |
custom_records[].address |
string | Yes | The IP address to resolve the name to. |
custom_records[].hostname |
string | No | Short hostname without the domain suffix. Adds a reverse DNS entry as well. |
Additional dnsmasq directives can be appended verbatim by placing plain-text files in data/dnsmasq/fragments/. Each file's contents are concatenated into the generated config. This is useful for advanced options not covered by the JSON schema (e.g., bogus-priv, cache-size, log-queries).
Nginx Configuration
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.
{
"domains": {
"app.example.com": {
"backend": {
"host": "192.168.2.50",
"port": 8080,
"proto": "http"
},
"force_ssl": true,
"headers": {
"X-Forwarded-Proto": "https",
"X-Real-IP": "$remote_addr"
},
"cert": {
"type": "acme",
"email": "admin@example.com"
}
}
},
"management": {
"domain": "<hostname>.local",
"backend": {
"host": "127.0.0.1",
"port": 9090,
"proto": "http"
},
"auth": {
"user": "admin",
"htpasswd": "data/nginx/.htpasswd"
}
},
"ssl": {
"protocols": "TLSv1.2 TLSv1.3",
"ciphers": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384",
"prefer_server_ciphers": false
}
}
Domain Entries
The domains object maps domain names (keys) to proxy configurations. Each entry produces a separate nginx server block.
| Field | Type | Required | Description |
|---|---|---|---|
backend |
object | Yes | The upstream service that receives proxied traffic. |
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 | Yes | Protocol for the backend connection: http or https. |
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 |
object | No | Certificate configuration for this domain. Required unless the management domain shares its cert. |
cert.type |
string | Yes (if cert) |
Certificate provisioning method. One of: acme, file, or selfsigned. |
cert.email |
string | Yes (if acme) |
ACME account email used by the CA provider. |
cert.path |
string | Yes (if file) |
Full path to the public certificate file (PEM). |
cert.key_path |
string | Yes (if file) |
Full path to the private key file (PEM). |
Certificate Types
| Type | Description |
|---|---|
acme |
Vacuum Wall uses acme.sh to request and renew an ACME certificate via the HTTP-01 challenge. The nginx configuration is temporarily modified to serve the ACME challenge files at /.well-known/acme-challenge/. The email field is required. |
file |
Use a pre-existing certificate and private key from the local file system. The path and key_path fields must point to readable PEM files. Vacuum Wall will not attempt to renew these certificates. |
selfsigned |
Vacuum Wall generates a self-signed certificate and private key on first apply. Useful for internal domains or testing. The generated certificate is stored at data/certs/. |
Management Domain
The management block configures the Vacuum Wall admin interface itself. It follows the same structure as a domain entry but includes an auth block for HTTP Basic Authentication.
| 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 | Yes | HTTP Basic Authentication configuration. |
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 .htpasswd file can be created with the htpasswd utility:
htpasswd -bc data/nginx/.htpasswd admin yourpassword
Global SSL Settings
The ssl block defines TLS parameters applied to all HTTPS server blocks via the shared snippet.
| Field | Type | Required | Description |
|---|---|---|---|
protocols |
string | No | nginx ssl_protocols directive value. Default: TLSv1.2 TLSv1.3. |
ciphers |
string | No | nginx ssl_ciphers directive value. Default is a curated AEAD-only cipher string. |
prefer_server_ciphers |
boolean | No | Whether to prefer server cipher order. Default: false. |
WireGuard Configuration
File: config/wireguard/config.json
This file defines the WireGuard server interface and all connected peers. The application renders it into /etc/wireguard/wg0.conf and applies it with wg-quick.
{
"interface": {
"name": "wg0",
"listen_port": 51820,
"private_key": "kOv8lK...',
"public_key": "YzP3xI...',
"addresses": ["10.137.0.1/24"],
"post_up": null,
"post_down": null
},
"peers": {
"alice": {
"public_key": "nR7mQ2...',
"private_key": "xLpDgF...',
"endpoint": "203.0.113.1:51820",
"allowed_ips": ["0.0.0.0/0"],
"persistent_keepalive": 25,
"preshared_key": null
}
}
}
Interface Fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | WireGuard interface name. Default: wg0. |
listen_port |
integer | Yes | Port the WireGuard interface listens on. Default: 51820. Must be opened in the firewall. |
private_key |
string | Yes | Base64-encoded private key for the server interface. Use wg genkey to generate. |
public_key |
string | Yes | Corresponding public key. Use wg pubkey to derive from the private key. |
addresses |
array | Yes | IP address(es) assigned to the server interface in CIDR notation (e.g., 10.137.0.1/24). |
post_up |
string | No | Shell command to run after the interface is brought up. Common uses: adding NAT rules, enabling IP forwarding for the tunnel. Set to null to omit. |
post_down |
string | No | Shell command to run after the interface is brought down. Used to clean up rules added by post_up. Set to null to omit. |
Peer Fields
Peers are stored in an object keyed by a human-readable identifier (e.g., alice, office-laptop). Each peer entry defines a WireGuard peer configuration.
| Field | Type | Required | Description |
|---|---|---|---|
public_key |
string | Yes | The peer's public key. |
private_key |
string | No | The peer's private key, stored for generating downloadable client configuration files. This value is stripped from all API responses — the WebUI never exposes peer private keys over the network. |
endpoint |
string | No | The peer's public endpoint (IP:port). Required for server-initiated connections (e.g., the server reaching out to a peer behind a firewall). Leave empty or null for peer-initiated connections where the peer connects to the server. |
allowed_ips |
array | No | CIDR blocks that traffic from this peer is allowed to route. Defaults to [] (no routing restrictions from the server side). ["0.0.0.0/0"] allows all traffic. ["10.137.0.0/16"] restricts traffic to the VPN subnet. |
persistent_keepalive |
integer | No | Keepalive interval in seconds. 25 is recommended for peers behind NAT. Set to 0 or null to disable. |
preshared_key |
string | No | Optional pre-shared key for post-quantum resistance. Use wg genpsk to generate. |
Client Configuration Generation
When a peer's private_key is set, the WebUI can generate a complete WireGuard client configuration file that the user can download and import into their WireGuard client app. The generated config includes the peer's interface settings, the server as a [Peer] entry, and the appropriate Endpoint and AllowedIPs values. The private_key field is written into the client config file for download but is never returned by the API.
Applying Configuration
When configuration is saved through the WebUI or API, the application:
- Validates all key pairs and IP ranges.
- Renders the
wg0.conffile from the JSON configuration. - Copies the rendered file to
/etc/wireguard/wg0.confusing the sudo whitelist. - Runs
sudo wg-quick up wg0to apply the configuration. - Returns success or error status to the caller.
If the interface is already up, wg-quick up will reconfigure it in place without dropping existing connections.