Files
vacuum-wall/docs/api.md
T
mteehan 65741644a3 Fix dashboard template bugs, acme date parsing, wireguard sudoers match, and stale docs
- dashboard.html: Fix zones, leases, wg_status, cert key names, add services var
- server.py: Pass services to dashboard template via _get_service_status()
- lib/acme.py: Fix dead third date format (%Y%m%d%H%M%z) using astimezone(UTC)
- lib/wireguard.py: Add -- separator to cp command to match sudoers rule
- lib/nginx.py: Replace shallow dict.copy() with {**...} for DEFAULT_SSL
- AGENTS.md: Update test count 149 -> 154
- docs/api.md: Rename cert field expiry -> expires_at
2026-05-08 19:11:54 +00:00

1021 lines
23 KiB
Markdown

# 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://wall.lan/api/...`).
Every request and response uses `Content-Type: application/json`.
## Conventions
### Success Responses
```json
{
"ok": true,
"data": <value>
}
```
The `data` field contains the payload, which may be an object, array, string, or `null`.
### Error Responses
```json
{
"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 |
---
## Firewall API
Endpoints prefixed with `/api/firewall/...`. Interact with firewalld for zone management, rich rules, NAT, and masquerade.
### 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: number, proto: string, toaddr: string, toport: number}, ...]` | Port forward rules |
| `rich_rules` | `[string, ...]` | Rich rule definitions |
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 to the zone |
---
#### 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 in the zone |
### Firewall Rules
#### Add Rich Rule
```
POST /api/firewall/rich-rules
```
Add a firewalld rich rule to a zone.
**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 |
| `rule` | `string` | Full rich rule string |
---
#### Remove Rich Rule
```
DELETE /api/firewall/rich-rules
```
Remove an existing rich rule from a zone. The `rule` string must match exactly.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone the rule belongs to |
| `rule` | `string` | Yes | Exact rich rule string to remove |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `zone` | `string` | Zone name |
| `rule` | `string` | Exact rich rule string that was removed |
---
#### List Rich Rules
```
GET /api/firewall/rich-rules/<zone>
```
Return all rich rules for the specified zone.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[string, ...]` | Rich rule strings |
### 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 for the zone |
---
#### Add Port Forward
```
POST /api/firewall/forward-port
```
Add a port forwarding rule to a zone.
**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 |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
---
#### Remove Port Forward
```
DELETE /api/firewall/forward-port
```
Remove a port forwarding rule. The body must match the original rule exactly.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zone` | `string` | Yes | Zone the rule belongs 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 |
| `port` | `number` | External port |
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
### 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.
### 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 |
---
#### 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 for the reservation |
---
#### Remove Static Lease
```
DELETE /api/dhcp/static-lease?mac=aa:bb:cc:dd:ee:ff
```
Remove a previously configured static lease.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mac` | `string` | Yes | MAC address of the lease to remove |
**Response:** `data` is `null` on success.
Returns HTTP `404` if no matching lease is found.
### 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 |
**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=nas.lan
```
Remove a custom DNS record.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Fully qualified domain name to remove |
**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"` |
**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`):**
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain name |
| `backend_host` | `string` | Backend server address |
| `backend_port` | `number` | Backend server port |
| `backend_proto` | `string` | Backend protocol |
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 the fields present in the body are modified.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `backend_host` | `string` | No | Backend server IP or hostname |
| `backend_port` | `number` | No | Backend server port |
| `backend_proto` | `string` | No | Backend protocol |
**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. Useful for validating changes before applying.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data.valid` | `boolean` | Whether the configuration syntax is valid |
| `data.output` | `string` | Raw nginx test output |
### 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., `"wall.lan"`) |
| `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. An `.htpasswd` entry is created when this field is present. |
| `auth_pass` | `string` | No | Password for basic auth. Used together with `auth_user`. |
**Response:** `data` is `null` on success.
If `auth_user` and `auth_pass` are provided, the endpoint creates or updates the corresponding `.htpasswd` file entry.
---
## Certificate API
Endpoints prefixed with `/api/certs/...`. Manage TLS certificates via ACME (Let's Encrypt / certbot).
### 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:
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain the certificate covers |
| `expires_at` | `string` | Expiration date (ISO 8601) |
| `days_until_expiry` | `number` | Remaining days until expiration |
| `cert_path` | `string` | Path to the certificate file |
| `key_path` | `string` | Path to the private key file |
---
#### Get Certificate Details
```
GET /api/certs/<domain>
```
Return details for a single certificate.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `domain` | `string` | Domain |
| `expires_at` | `string` | Expiration date (ISO 8601) |
| `days_until_expiry` | `number` | Remaining days |
| `cert_path` | `string` | Certificate file path |
| `key_path` | `string` | Private key file 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 or the request is malformed. Returns HTTP `500` if the ACME challenge or certificate issuance fails.
---
#### Renew Certificate
```
POST /api/certs/<domain>/renew
```
Force-renew an existing certificate, regardless of its current expiry status.
**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 (used by Let's Encrypt for expiration and security notices).
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | `string` | Yes | Contact email address |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `email` | `string` | Contact email address |
---
## 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 from the response.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `object` | Full WireGuard configuration dictionary (`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:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `object` | Updated configuration (`private_key` omitted) |
### Tunnel Control
#### Apply Configuration
```
POST /api/wireguard/apply
```
Write the current configuration to `wg0.conf` on disk and bring the WireGuard tunnel up.
**Response:** `data` is `null` on success.
Returns HTTP `500` if config write or interface bring-up fails.
---
#### 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, including 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, etc.) |
| `peers` | `[object, ...]` | Per-peer connection stats (handshake time, transfer bytes, endpoint, etc.) |
---
#### Initialize
```
POST /api/wireguard/initialize
```
Perform first-time setup: generate a server key pair, write an initial configuration, and prepare for peer enrollment. This endpoint is idempotent — calling it multiple times has no additional effect.
**Response:** `data` is `null` on success.
### Peer Management
#### Add Peer
```
POST /api/wireguard/add-peer
```
Add a new WireGuard peer. A key pair is auto-generated for the peer. The response includes peer details with the private key stripped.
**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 in seconds |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Peer name |
| `public_key` | `string` | Peer's public key |
| `allowed_ips` | `[string, ...]` | Allowed IPs |
| `endpoint` | `string` | Allowed endpoint |
| `persistent_keepalive` | `number` | Keepalive interval |
---
#### Remove Peer
```
DELETE /api/wireguard/remove-peer?name=alice
```
Remove a configured peer.
**Query Parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Peer name to remove |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Peer name |
Returns HTTP `404` if the peer is not found.
---
#### List Peers
```
GET /api/wireguard/peers
```
Return all configured peers. Private keys are stripped from the response.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of peer objects (private keys omitted) |
---
#### Peer Connection Status
```
GET /api/wireguard/peer-status
```
Return live per-peer connection status from `wg show`, including last handshake time, transfer bytes, and current endpoint.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `[object, ...]` | Array of live peer status objects |
### Client Configuration
#### Generate Client Config
```
POST /api/wireguard/generate-client
```
Generate a complete WireGuard client configuration file for provisioning a device. The returned config includes the peer's private key for the client to use.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Peer name to generate config for |
| `server_endpoint` | `string` | Yes | Server public address (`"ip:port"`) for the client's `[Peer]` section |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `config` | `string` | Complete WireGuard client config text (`[Interface]` + `[Peer]` block) |
The client config includes the generated private key so the client can be provisioned directly. Note that 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.