Files
abrown38andClaude Opus 5.5 3c0808591f Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
Subnets served per interface, pools, reservations, options at three
levels, leases in SQLite saved before the ACK, ping and decline conflict
checks, validated changes with history and undo, users and tokens, an
audit log, a Kea importer, a sandboxed systemd unit and Debian packages.
Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:14:07 +01:00

5.6 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), with steward token create NAME, or with stewardd user token USER NAME on the server.
  • Sessions: POST /auth/login with {"username", "password"} sets a cookie and returns csrf_token. Requests other than GET must send it as X-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=1 checks the change and returns what it would do, with "applied": false, "dry_run": true.
  • before is missing for a create and after for a delete.
  • warnings are the problems the change brings; older ones are in /status.
  • object is 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
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, "options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}

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"], "lease_time": 0,
 "options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}

pools and options replace the lists; 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", "comment"}; ip and mac or client_id needed
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; 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"}
GET /events[?mac=&interface=&limit=] the latest DHCP messages, newest first (the last 1000 are kept in memory)

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

Users, tokens, audit

POST /auth/login, POST /auth/logout, GET /auth/whoami
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
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=]