- A subnet is a deny-list subnet (the default: everyone not denied) or
an allow-list subnet (only clients allowed or with a reservation);
the deny list counts in both. Entries are MAC addresses, prefixes
(aa:bb:cc:*) or client IDs. A refused client gets no answer (DENIED
in the activity log, with an Allow button) or a NAK on renewal.
steward access, the subnet's Access tab. Schema migration 7.
- The activity log keeps each message's header fields and options,
decoded into words (dhcp4.Describe): the parameter request list by
name, client IDs, lease times. ▸ on an Activity row, steward
activity show N / --details, GET /events/{id}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.0 KiB
The REST API
Everything the web UI and the CLI do goes through this API, under
/api/v1. Requests and answers are JSON.
Authentication
- API tokens:
Authorization: Bearer steward_.... Make one in the web UI (your account), withsteward token create NAME, or withstewardd user token USER NAMEon the server. - Sessions:
POST /auth/loginwith{"username", "password"}sets a cookie and returnscsrf_token. Requests other than GET must send it asX-CSRF-Token. - The local socket (
/run/steward/steward.sock): root and stewardd's own user need nothing; others send a token.
Failed logins and token attempts are rate limited per address and per user name. permissions.md has what each call needs.
Errors
{"error": {"code": "validation", "message": "subnet iot (10.0.20.0/24): it overlaps subnet office (10.0.10.0/24)",
"problems": [{"severity": "error", "code": "subnet_overlap", "message": "...", "object": "subnet iot (10.0.20.0/24)"}]}}
| Status | Code | |
|---|---|---|
| 400 | bad_request |
the request can't be read (an unknown option name, a bad address) |
| 401 | unauthenticated, session_expired, login_failed |
|
| 403 | forbidden, csrf |
|
| 404 | not_found |
|
| 409 | conflict |
e.g. undoing a change after the objects changed again |
| 422 | validation |
the change would make the configuration invalid; problems lists why |
| 429 | rate_limited |
Changes
Calls that change the configuration answer:
{"applied": true, "history_id": 7,
"changes": [{"kind": "subnet", "id": 12, "label": "iot (10.0.20.0/24)", "before": {...}, "after": {...}}],
"warnings": [{"severity": "warning", "code": "interface_unknown", "message": "...", "object": "..."}],
"object": {...}}
?dry_run=1checks the change and returns what it would do, with"applied": false, "dry_run": true.beforeis missing for a create andafterfor a delete.warningsare the problems the change brings; older ones are in/status.objectis the created or changed object.
Creating answers 201, other changes 200.
Endpoints
Status
GET /healthz |
{"ok": true, "version"}, without authentication |
GET /status |
build, start time, interfaces (listening, error, addresses, subnets), subnets with usage, problems, leases (counts by state), messages (counts by type), ping_problem |
GET /interfaces |
the host's interfaces, their addresses and subnets, leaving out those matching interfaces.exclude |
GET /catalog |
the option catalog, value types, reserved codes, roles |
Settings
GET /settings, PATCH /settings with any of:
{"lease_time": 43200, "ping_check": true, "ping_timeout_ms": 500, "decline_hold": 86400,
"not_authoritative": false, "address_order": "sequential",
"options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}
address_order is sequential (left out when it is), reverse or
random (subnets.md).
options replaces the list. Each option is {"option": NAME or NUMBER, "value": TEXT}, with "type" for numbers the catalog doesn't know
(options.md).
Subnets
GET /subnets |
every subnet, with usage (pool_size, leased, declined, reservations, reservations_leased) |
POST /subnets |
create; needs prefix and interface |
GET /subnets/{ref} |
by ID, name, or prefix with _ for / (10.0.10.0_24) |
PATCH /subnets/{ref} |
change the given fields |
DELETE /subnets/{ref} |
delete, with its reservations |
{"name": "office", "interface": "eth0", "prefix": "10.0.10.0/24",
"pools": ["10.0.10.100-10.0.10.199", "10.0.10.224/28:random"], "pool_order": "listed",
"address_order": "default", "lease_time": 0,
"options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}
access is deny-list (the default, left out) or allow-list; allow
and deny replace the lists, each [{"match": "aa:bb:cc:*", "comment": "Sonos"}] with a MAC address, a prefix or "id HEX"
(subnets.md).
pools and options replace the lists; the pools keep their order. A
pool's :ORDER is its own address order. pool_order is listed or
random; address_order is sequential, reverse, random or
default (the settings'). Subnets come back with pools as
{"start", "end", "order"}, and the defaults (listed, default) left
out. lease_time 0 means the settings'.
Reservations
GET /reservations[?subnet=REF] |
each with subnet_id, subnet and its current lease |
POST /reservations |
{"ip", "mac", "client_id", "hostname", "options", "lease_time", "disabled", "comment"}; ip and mac or client_id needed (with both, either matches); lease_time 0 is the subnet's |
GET, PATCH, DELETE /reservations/{ref} |
by ID, address, or MAC address (when it has one reservation) |
Leases and activity
GET /leases[?subnet=&state=&interface=&q=] |
every lease, with its state now (active, expired, released, declined), subnet and its reservation, and last_seen when the client last answered a ping (left out if it never has); q searches address, MAC and host name |
GET /leases/{ip} |
one |
DELETE /leases/{ip} |
forget it (audited) |
POST /leases/{ip}/reserve |
make it a reservation; optional {"hostname", "comment", "by"}, by being mac, client_id or both (the default) |
GET /events[?mac=&subnet=&interface=&limit=&details=1] |
the latest DHCP messages, newest first (the last 1000 are kept in memory), each with an id, direction (in from the client, out from Steward) and client_id. subnet (ID, name or prefix) keeps messages for its addresses, and those without an address on its interface. details=1 adds fields and options |
GET /events/{id} |
one message with fields (header fields that are set) and options, each {"code", "name", "value"} in words |
History and import
GET /history[?limit=&before=] |
changes, newest first |
GET /history/{id} |
one, with each object before and after |
POST /history/{id}/revert |
undo it; 409 if an object it touched has changed since, or it was undone already |
POST /import/kea |
{"config": TEXT, "leases": [TEXT, ...], "interfaces": {"PREFIX": "IFACE"}, "replace": false} (kea.md); answers with leases, notes, skipped too |
Certificate
Administrators only (certificates.md).
GET /certificate |
{"configured", "source": "acme"|"file"|"none", "status": {"domain", "directory", "staging", "state": "none"|"ok"|"working"|"error", "not_before", "not_after", "issuer", "renew_at", "last_error", "last_attempt", "next_attempt", "check"}} |
POST /certificate/check |
run the check now (up to a few minutes); answers {"time", "domain", "ok", "steps": [{"name", "status", "detail", "queries"}]} |
POST /certificate/renew |
get a new certificate now, in the background (202); follow it with GET /certificate |
GET /status lists the certificate's problems (certificate_failed,
certificate_expiring, certificate_staging) for administrators.
Users, tokens, audit
POST /auth/login, POST /auth/logout, GET /auth/whoami |
whoami has token_id when the caller uses a token |
POST /me/password |
{"current", "password"} |
GET /me/tokens, POST /me/tokens |
{"name", "role", "grants", "expires_in"}; the answer's secret is shown once |
GET /tokens, DELETE /tokens/{id} |
all tokens (administrators), revoke |
POST /tokens/{id}/rotate |
a new secret: {"grace": "1h", "expires_in": "90d", "no_expiry": false}, all optional; answers with secret and previous_secret_valid_until |
GET, POST /users; PATCH, DELETE /users/{id} |
administrators; {"username", "password", "admin", "role", "grants", "disabled", "comment"} |
GET, POST /users/{id}/tokens |
|
GET /audit[?user=&action=&result=&q=&before=&limit=] |