FreeRADIUS REST API

A FastAPI service exposing RESTful CRUD over the FreeRADIUS MySQL/MariaDB schema (customers, radcheck, radreply, radgroupreply/vlans, radusergroup, nas, plus read-only radacct, radpostauth, nasreload).

Stack

  • FastAPI + Uvicorn (ASGI)
  • SQLAlchemy 2.0 ORM + PyMySQL driver
  • Pydantic v2 request/response validation
  • Auth via a shared secret in the X-API-Key header

Setup

python3 -m venv venv
venv/bin/pip install -r requirements.txt
cp .env.example .env      # then edit credentials + API_KEY

.env

Var Meaning
DB_HOST MySQL host (127.0.0.1 if API runs on the DB box)
DB_PORT MySQL port (default 3306)
DB_USER / DB_PASSWORD DB credentials
DB_NAME Database name (radius)
API_KEY Shared secret required in X-API-Key
CORS_ORIGINS Comma-separated allowed origins (* for dev)
DEFAULT_LIMIT / MAX_LIMIT List pagination caps

Note: MariaDB on the staging box binds to 127.0.0.1 only. To reach it from another host either run the API on the RADIUS server (DB_HOST=127.0.0.1), open an SSH tunnel (ssh -N -L 13306:127.0.0.1:3306 root@HOST then DB_PORT=13306), or bind MariaDB to the LAN and grant remote access.

Run

venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
  • Interactive docs (Swagger): http://HOST:8000/docs
  • OpenAPI JSON: http://HOST:8000/openapi.json
  • Health check (no auth): GET /health

Auth

Every resource endpoint requires the header:

X-API-Key: <your API_KEY>

Missing/wrong key → 401. /, /health, and /docs are open.

Endpoints

All list endpoints return a paginated envelope and accept ?limit=&offset= plus per-resource filters:

{ "total": 12, "limit": 50, "offset": 0, "items": [ ... ] }
Resource Path Methods Filters
Customers /customers GET, POST, PUT, DELETE username, mac_address, status
NAS clients /nas GET, POST, PUT, DELETE nasname, shortname
User check /radcheck GET, POST, PUT, DELETE username, attribute
User reply /radreply GET, POST, PUT, DELETE username, attribute
Group check /radgroupcheck GET, POST, PUT, DELETE groupname, attribute
Group reply /radgroupreply GET, POST, PUT, DELETE groupname, attribute
VLANs /vlan see below
Devices /device see below
User↔group /radusergroup GET, POST, PUT, DELETE username, groupname
Accounting /radacct GET (read-only) username, nasipaddress, active
Post-auth log /radpostauth GET (read-only) username, reply
NAS reload /nasreload GET (read-only)

radacct, radpostauth, and nasreload are read-only — FreeRADIUS owns writes.

VLANs — /vlan

A VLAN is a logical entity (alias + vlanid) backed by three radgroupreply rows sharing one groupname: Tunnel-Type=VLAN, Tunnel-Medium-Type=IEEE-802, and Tunnel-Private-Group-Id=<vlanid>.

Action Request
List VLANs GET /vlan/[{"alias","vlanid"}, ...]
Add a VLAN POST /vlan/add body {"vlanid":55,"alias":"staff"} (inserts the 3 rows)
Rename alias POST /vlan/edit body {"vlanid":55,"alias":"employees"}
Delete a VLAN DELETE /vlan/{vlanid} (removes all rows for that group)
  • List returns one row per VLAN — only the alias and VLAN ID, not the raw Tunnel-* attribute rows.
  • Duplicate VLAN ID or alias on add → 409.
  • vlanid must be 14094.
  • Renaming updates radgroupreply.groupname only; if you also map users to groups in radusergroup, update those separately.

Devices — /device

A device is a client identified by its MAC address (used as the RADIUS username). One device spans three tables: radcheck (MAC = password), radusergroup (group membership), and customers (status). MAC input is normalized to uppercase, hyphen-separated (AA-BB-CC-DD-EE-FF); colons and lowercase are accepted.

Action Request
List devices GET /device/{total,limit,offset,items:[{mac_address,group,status}]}
Get one device GET /device/{mac_address}
Add a device POST /device/add body {"mac_address":"14-99-3E-74-CB-7F","group":"staff"}
Edit a device POST /device/edit body {"mac_address":"...","group":"...","status":"..."}
Delete a device DELETE /device/{mac_address} (removes all 3 rows)
  • Add inserts a radcheck password (Cleartext-Password := MAC), a radusergroup row (priority 1), and a customers row (status = paid). The group must already exist in radgroupreply or you get 400.
  • Edit accepts group and/or status — supply either or both (at least one required). A new group must exist in radgroupreply (400 otherwise). status must be one of new / paid / unpaid. In the DB the group is stored in the radusergroup.groupname column.
  • Adding a device whose MAC already exists → 409.

Examples

Replace host/key to match your deployment.

# List customers
curl http://10.0.1.235:8000/customers?limit=50 \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# List VLANs
curl http://10.0.1.235:8000/vlan/ \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# Add a VLAN (alias "staff", VLAN ID 55)
curl http://10.0.1.235:8000/vlan/add \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"vlanid":55,"alias":"staff"}' -s | jq

# Rename a VLAN's alias
curl http://10.0.1.235:8000/vlan/edit \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"vlanid":55,"alias":"employees"}' -s | jq

# Delete VLAN 55
curl http://10.0.1.235:8000/vlan/55 \
 -X DELETE \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# List devices
curl http://10.0.1.235:8000/device/ \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# Add a device (MAC + existing group)
curl http://10.0.1.235:8000/device/add \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"mac_address":"14-99-3E-74-CB-7F","group":"staff"}' -s | jq

# Edit a device (any one field: groupname and/or status)
curl http://10.0.1.235:8000/device/edit \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"mac_address":"14-99-3E-74-CB-7F","status":"unpaid"}' -s | jq

# Delete a device
curl http://10.0.1.235:8000/device/14-99-3E-74-CB-7F \
 -X DELETE \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# Create a customer
curl http://10.0.1.235:8000/customers \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"username":"AA-BB-CC-DD-EE-FF","mac_address":"AA-BB-CC-DD-EE-FF","status":"new"}' -s | jq

# Update a customer's status
curl http://10.0.1.235:8000/customers/1 \
 -X PUT \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" \
 -H "Content-Type: application/json" \
 -d '{"status":"paid"}' -s | jq

# Active accounting sessions (no stop time)
curl http://10.0.1.235:8000/radacct?active=true \
 -H "X-API-Key: staging-dev-key-change-me-7f3a9c1e5b" -s | jq

# Health check (no key needed)
curl http://10.0.1.235:8000/health -s | jq

Status codes

Code Meaning
200 OK
201 Created
204 Deleted (no content)
401 Missing/invalid X-API-Key
404 Row not found
409 Duplicate / integrity conflict
422 Request body failed validation

Project layout

app/
  main.py            FastAPI app, router wiring, auth + CORS
  config.py          env-driven settings (pydantic-settings)
  database.py        SQLAlchemy engine/session
  auth.py            X-API-Key dependency
  errors.py          APIError + JSON handler
  crud.py            generic list/get/create/update/delete helpers
  pagination.py      Page envelope + limit/offset dependency
  models.py          SQLAlchemy models (one per table)
  schemas.py         Pydantic request/response models
  routers/           one module per resource
S
Description
RADIUS Admin. API and UI for SAR Link admins to mange devices and Radius on the network
Readme
366 KiB
Languages
TypeScript 55.8%
Python 42.1%
CSS 1.3%
HTML 0.4%
Dockerfile 0.3%