- wireguard: POST /peers with JSON encoding (was /add-peer) - rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render - nat: port forward delete uses URL path params to match blueprint - nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch) - app.js renderers updated to use URL path deletes for rules and forwards - remove TODO.md
24 KiB
REST API Reference
All endpoints are served by the management WebUI Flask application bound to 127.0.0.1:9090, proxied through nginx with SSL termination and HTTP basic authentication. Requests target the management domain (e.g., https://<hostname>.local/api/...).
Every request and response uses Content-Type: application/json.
Conventions
Success Responses
{
"ok": true,
"data": <value>
}
The data field contains the payload, which may be an object, array, string, or null.
Error Responses
{
"ok": false,
"error": "<human-readable message>"
}
Error responses carry one of the following HTTP status codes:
| Code | Meaning |
|---|---|
400 |
Bad request — invalid body, missing required field, or malformed value |
404 |
Not found — the requested resource does not exist |
500 |
Internal server error — unexpected failure in the backend |
Route Patterns
Resource identification uses path parameters whenever possible. Exceptions occur only when the identifier is inherently long (e.g., a rich rule string), in which case the body carries the identifier.
Firewall API
Endpoints prefixed with /api/firewall/.... Interact with firewalld for zone management, rich rules, NAT, and masquerade.
Declarative Config
The firewall supports a two-step declarative workflow: save config to config/firewall/config.json, then apply it to live firewalld. The config tracks rich_rules and forward_ports with auto-generated id fields.
Get Config
GET /api/firewall/config
Return the current declarative firewall config.
Response: data contains the config object with a zones mapping.
Save Config
POST /api/firewall/config
Replace the declarative config. Returns pending changes summary.
Request Body: Request body must contain zones.
Response (data):
| Field | Type | Description |
|---|---|---|
config_saved |
boolean |
Always true |
pending |
[object, ...] |
List of pending changes |
needs_apply |
boolean |
Whether changes need to be applied |
unmanaged_zones |
object |
Zones active on system but not in config |
Apply Config
POST /api/firewall/config/apply
Apply the declarative config to live firewalld. Applies targets, services, interfaces, masquerade, rich rules, and forward ports.
Response: data contains applied_zones list and backup path.
Check Pending Changes
GET /api/firewall/config/pending
Compare declarative config against live firewalld state. Returns diff for interfaces, services, targets, masquerade, rich rules, and forward ports.
Response: Same structure as POST /config response.
Partial Update Config
PATCH /api/firewall/config
Deep-merge the provided fields into the existing config.
Response: data is null on success.
Zone Management
List All Zones
GET /api/firewall/zones
Returns active zone-to-interface mappings and all available zone definitions.
Response:
| Field | Type | Description |
|---|---|---|
data.active |
object<name, [interface, ...]> |
Currently assigned interfaces per zone |
data.available |
[string, ...] |
All zones known to firewalld |
Get Zone Details
GET /api/firewall/zones/<name>
Return detailed configuration for a single zone.
Response (data):
| Field | Type | Description |
|---|---|---|
target |
string |
Zone target (e.g., "default", "ACCEPT", "REJECT") |
interfaces |
[string, ...] |
Interfaces assigned to this zone |
services |
[string, ...] |
Services allowed through the zone |
ports |
[string, ...] |
Explicit port rules (format: "443/tcp") |
masquerade |
boolean |
Whether masquerade (NAT) is enabled |
forward_ports |
[{port, proto, toaddr, toport}, ...] |
Port forward rules |
rich_rules |
[{rule, id}, ...] |
Rich rule definitions with IDs |
Returns HTTP 404 if the zone does not exist.
Create Zone
POST /api/firewall/zones
Create a new firewalld zone.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Zone name |
target |
string |
No | Zone target; defaults to "default" |
Response: data is null on success.
Delete Zone
DELETE /api/firewall/zones/<name>
Remove a zone from firewalld.
Response: data is null on success.
Returns HTTP 404 if the zone does not exist.
Zone Configuration
Set Zone Interfaces
POST /api/firewall/zones/<name>/interfaces
Replace all interfaces assigned to the zone with the provided list.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
interfaces |
[string, ...] |
Yes | List of interface names |
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
interfaces |
[string, ...] |
List of interface names now assigned |
Set Zone Services
POST /api/firewall/zones/<name>/services
Replace all services allowed in the zone with the provided list.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
services |
[string, ...] |
Yes | List of firewalld service names |
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
services |
[string, ...] |
List of services now allowed |
Rich Rules
Add Rich Rule
POST /api/firewall/rich-rules
Add a firewalld rich rule to a zone. The rule is persisted to the declarative config with an auto-generated id.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
zone |
string |
Yes | Zone to add the rule to |
rule |
string |
Yes | Full rich rule string |
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
id |
string |
8-character unique ID |
rule |
string |
Full rich rule string |
Remove Rich Rule
DELETE /api/firewall/rich-rules/<zone>/<id>
Remove a rich rule by zone and auto-generated ID. (The rule string itself is too long for a URL path.)
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
id |
string |
ID of the removed rule |
Returns HTTP 404 if the rule ID is not found.
List Rich Rules
GET /api/firewall/rich-rules/<zone>
Return all rich rules for the specified zone, each with an id and rule string.
Response:
| Field | Type | Description |
|---|---|---|
data |
[{id, rule}, ...] |
Rich rules with IDs |
Port Forwarding
Add Port Forward
POST /api/firewall/forward-port
Add a port forwarding rule to a zone. The rule is persisted to the declarative config with an auto-generated id.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
zone |
string |
Yes | Zone to add the rule to |
port |
number |
Yes | External port |
proto |
string |
Yes | Protocol ("tcp" or "udp") |
toaddr |
string |
No | Internal destination address |
toport |
number |
No | Internal destination port |
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
id |
string |
8-character unique ID |
port |
number |
External port |
proto |
string |
Protocol ("tcp" or "udp") |
Remove Port Forward
DELETE /api/firewall/forward-port/<zone>/<port>/<proto>
Remove a port forwarding rule. Zone, port, and protocol are all path parameters.
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
port |
number |
External port |
proto |
string |
Protocol ("tcp" or "udp") |
Returns HTTP 404 if the forward port is not found.
Masquerade (NAT)
Enable / Disable Masquerade
POST /api/firewall/masquerade
Toggle masquerade (source NAT) for a zone.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
zone |
string |
Yes | Zone to configure |
enable |
boolean |
Yes | true to enable, false to disable |
Response (data):
| Field | Type | Description |
|---|---|---|
zone |
string |
Zone name |
masquerade |
boolean |
Whether masquerade is now enabled |
Info
Available Services
GET /api/firewall/services
List all service names known to firewalld.
Response:
| Field | Type | Description |
|---|---|---|
data |
[string, ...] |
Service names |
Available Interfaces
GET /api/firewall/interfaces
List all network interfaces currently available on the system.
Response:
| Field | Type | Description |
|---|---|---|
data |
[string, ...] |
Interface names |
DHCP / DNS API
Endpoints prefixed with /api/dhcp/.... Manage dnsmasq configuration, DHCP leases, and custom DNS records.
Configuration
Get Configuration
GET /api/dhcp/config
Return the current DHCP/DNS configuration object.
Response:
| Field | Type | Description |
|---|---|---|
data |
object |
Full dnsmasq configuration dictionary |
Replace Configuration
POST /api/dhcp/config
Replace the entire configuration with the provided JSON object.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| (entire body) | object |
Yes | Complete configuration object |
Response: data is null on success.
Partial Update Configuration
PATCH /api/dhcp/config
Deep-merge the provided fields into the existing configuration. Useful for targeted updates (e.g., changing DNS upstream servers without replacing the full config).
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| (any subset) | any |
Yes | Fields to merge into the existing config |
Response: data is null on success.
Apply Configuration
POST /api/dhcp/apply
Write the in-memory configuration to /etc/dnsmasq.d/vacuum-wall.conf and reload the dnsmasq service.
Response: data is null on success.
Status
Get Service Status
GET /api/dhcp/status
Return the current service status, config summary, and active lease count.
Response (data):
| Field | Type | Description |
|---|---|---|
service_active |
boolean |
Whether dnsmasq is running |
config_file_exists |
boolean |
Whether config file exists on disk |
config_in_sync |
boolean |
Whether disk config matches expected |
dhcp_ranges |
number |
Number of DHCP ranges |
static_leases |
number |
Number of static leases |
custom_dns_records |
number |
Number of custom DNS records |
upstreams |
[string, ...] |
Upstream DNS servers |
domain |
string |
Local DNS domain |
active_leases |
number |
Number of active leases |
leases |
[object, ...] |
Active lease objects |
DHCP Ranges
Add Range
POST /api/dhcp/ranges
Add or replace the DHCP range for a given interface.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
interface |
string |
No | Interface name (empty = all interfaces) |
start |
string |
Yes | Start of IP range |
end |
string |
Yes | End of IP range |
lease_time |
string |
No | Lease duration; defaults to "12h" |
Response: data is null on success.
Remove Range
DELETE /api/dhcp/ranges
Remove a DHCP range. Body contains identifying fields.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
interface |
string |
Yes | Interface name |
start |
string |
Yes | Start of IP range |
end |
string |
Yes | End of IP range |
Response: data is null on success.
Static Leases
Add Static Lease
POST /api/dhcp/static-lease
Add a static (reserved) DHCP lease.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
mac |
string |
Yes | MAC address ("aa:bb:cc:dd:ee:ff") |
ip |
string |
Yes | Reserved IP address |
hostname |
string |
No | Hostname for the reservation |
Response (data):
| Field | Type | Description |
|---|---|---|
mac |
string |
MAC address |
ip |
string |
Reserved IP address |
hostname |
string |
Hostname |
Remove Static Lease
DELETE /api/dhcp/static-lease/<mac>
Remove a static lease by MAC address.
Response: data is null on success.
Returns HTTP 404 if no matching lease is found.
Live Leases
Get Live Leases
GET /api/dhcp/leases
Return the current DHCP lease table from dnsmasq.
Response:
| Field | Type | Description |
|---|---|---|
data |
[object, ...] |
Array of lease objects |
DNS Records
Add DNS Record
POST /api/dhcp/dns-record
Add a custom DNS A record served by dnsmasq.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Fully qualified domain name |
address |
string |
Yes | IP address to resolve to |
hostname |
string |
No | Short hostname |
Response (data):
| Field | Type | Description |
|---|---|---|
name |
string |
Fully qualified domain name |
address |
string |
IP address |
hostname |
string |
Short hostname |
Remove DNS Record
DELETE /api/dhcp/dns-record/<name>
Remove a custom DNS record by domain name.
Response: data is null on success.
Returns HTTP 404 if no matching record is found.
Proxy API
Endpoints prefixed with /api/proxy/.... Manage reverse proxy domains, nginx configuration generation, and the management WebUI proxy.
Domain Management
List All Domains
GET /api/proxy/domains
Return all configured proxy domains.
Response:
| Field | Type | Description |
|---|---|---|
data |
[object, ...] |
Array of domain configuration objects |
Add Domain
POST /api/proxy/domains
Add a new reverse proxy domain.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string |
Yes | Domain name to proxy |
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 |
extra_headers |
object |
No | Extra proxy headers |
Response (data):
| Field | Type | Description |
|---|---|---|
domain |
string |
Domain name |
Returns HTTP 400 if the domain is already configured.
Get Domain Details
GET /api/proxy/domains/<domain>
Return the configuration for a single proxy domain.
Response (data): Domain name plus backend configuration fields.
Returns HTTP 404 if the domain is not configured.
Update Domain
PUT /api/proxy/domains/<domain>
Update one or more fields of an existing domain entry. Only fields present in the body are modified.
Request Body: Any subset of (backend_host, backend_port, backend_proto, cert, extra_headers).
Response (data):
| Field | Type | Description |
|---|---|---|
domain |
string |
Domain name |
Returns HTTP 404 if the domain is not configured.
Remove Domain
DELETE /api/proxy/domains/<domain>
Remove a proxy domain and its nginx configuration.
Response (data):
| Field | Type | Description |
|---|---|---|
domain |
string |
Domain name |
Returns HTTP 404 if the domain is not configured.
Apply / Test
Apply Configuration
POST /api/proxy/apply
Regenerate nginx configuration files for all proxy domains and reload the nginx service.
Response: data is null on success.
Returns HTTP 500 if nginx config generation fails or the reload fails.
Test Configuration
POST /api/proxy/test
Run nginx -t against the generated configuration without reloading.
Response (valid):
| Field | Type | Description |
|---|---|---|
data.valid |
boolean |
Always true |
data.output |
string |
Raw nginx test output |
Error (invalid): HTTP 400 with standard {"ok": false, "error": "<nginx output>"} response.
Management
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.
Certificate API
Endpoints prefixed with /api/certs/.... Manage TLS certificates via ACME (ZeroSSL, Let's Encrypt, etc.).
Listing & Details
List All Certificates
GET /api/certs/list
Return all managed certificates with metadata.
Response:
| Field | Type | Description |
|---|---|---|
data |
[object, ...] |
Array of certificate objects |
Each certificate object contains domain, expires_at, days_until_expiry, cert_path, key_path.
Get Certificate Details
GET /api/certs/<domain>
Return details for a single certificate.
Response (data): Fields: domain, expires_at, days_until_expiry, cert_path, key_path.
Returns HTTP 404 if no certificate is found for the domain.
Operations
Issue Certificate
POST /api/certs/issue
Request a new certificate for a domain.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
domain |
string |
Yes | Domain to issue the certificate for |
webroot |
string |
No | Custom webroot path for HTTP-01 validation |
Response: data is null on success.
Returns HTTP 400 if the domain is missing. Returns HTTP 500 if issuance fails.
Renew Certificate
POST /api/certs/<domain>/renew
Force-renew an existing certificate.
Response: data is null on success.
Returns HTTP 404 if the certificate is not found. Returns HTTP 500 if renewal fails.
Remove Certificate
DELETE /api/certs/<domain>
Delete a certificate and remove it from auto-renewal tracking.
Response: data is null on success.
Returns HTTP 404 if the certificate is not found.
Account
Set ACME Contact Email
POST /api/certs/email
Set or update the ACME account contact email.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string |
Yes | Contact email address |
Response (data): Returns the set email field.
WireGuard API
Endpoints prefixed with /api/wireguard/.... Manage the WireGuard VPN server, peers, and client configuration.
Configuration
Get Configuration
GET /api/wireguard/config
Return the current WireGuard server configuration. The private_key field is stripped.
Response:
| Field | Type | Description |
|---|---|---|
data |
object |
WireGuard config (private_key omitted) |
Replace Configuration
POST /api/wireguard/config
Replace the entire WireGuard configuration. The private_key field is stripped from the response.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| (entire body) | object |
Yes | Complete WireGuard configuration object |
Response: data contains the updated configuration (private_key omitted).
Tunnel Control
Apply Configuration
POST /api/wireguard/apply
Write the current configuration to wg0.conf and bring the tunnel up.
Response: data is null on success.
Start Tunnel
POST /api/wireguard/up
Alias for /api/wireguard/apply — write config and bring the tunnel up.
Response: data is null on success.
Bring Tunnel Down
POST /api/wireguard/down
Bring down the WireGuard tunnel interface (wg0).
Response: data is null on success.
Status
Tunnel Status
GET /api/wireguard/status
Return live tunnel state with interface metrics and per-peer connection statistics.
Response (data):
| Field | Type | Description |
|---|---|---|
up |
boolean |
Whether the tunnel interface is up |
interface |
object |
Interface info (listen port, public key) |
peers |
[object, ...] |
Per-peer stats (handshake, bytes, endpoint) |
Initialize
POST /api/wireguard/initialize
First-time setup: generate server key pair, write initial config. Idempotent.
Response: data is null on success.
Peer Management
List Peers
GET /api/wireguard/peers
Return all configured peers. Private keys are stripped.
Response:
| Field | Type | Description |
|---|---|---|
data |
[object, ...] |
Peer objects (private keys omitted) |
Add Peer
POST /api/wireguard/peers
Add a new WireGuard peer. A key pair is auto-generated. Private key stripped from response.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Peer identifier name |
endpoint |
string |
No | Allowed endpoint address ("ip:port") |
allowed_ips |
[string, ...] |
No | Allowed IPs; defaults to ["0.0.0.0/0"] |
persistent_keepalive |
number |
No | Persistent keepalive interval (seconds) |
preshared_key |
string |
No | Preshared key |
Response (data): Peer object with name, public_key, allowed_ips, etc. (no private_key).
Remove Peer
DELETE /api/wireguard/peers/<name>
Remove a configured peer by name.
Response (data):
| Field | Type | Description |
|---|---|---|
name |
string |
Peer name |
Returns HTTP 404 if the peer is not found.
Peer Connection Status
GET /api/wireguard/peer-status
Return live per-peer connection status from wg show.
Response:
| Field | Type | Description |
|---|---|---|
data |
[object, ...] |
Live peer status (handshake time, bytes, endpoint) |
Client Configuration
Generate Client Config
POST /api/wireguard/generate-client
Generate a complete WireGuard client configuration file. The returned config includes the peer's private key for provisioning.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Peer name to generate config for |
server_endpoint |
string |
Yes | Server public address ("ip:port") |
Response (data):
| Field | Type | Description |
|---|---|---|
config |
string |
Complete client config text ([Interface] + [Peer]) |
This is the only endpoint that returns a WireGuard private key. All other endpoints strip private keys from responses.
Returns HTTP 404 if the peer is not found.