Files
vacuum-wall/docs/api.md
T
mteehan d1ab717c0f refactor: unify project structure, improve security, and enhance deployment
- 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
2026-05-25 00:53:32 +00:00

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. Returns pending changes summary.

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

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).


Partial Update Configuration

PATCH /api/wireguard/config

Deep-merge the provided fields into the existing configuration.

Request Body:

Field Type Required Description
(any subset) any Yes Fields to merge into the existing config

Response: data is null on success.

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.