A lightweight web admin console for managing a Kanidm identity provider. Built for small VPS/homelab deployments.
- Users: List, search, create, disable, delete users
- Groups: List, search, create, delete groups; manage membership
- OAuth2 Applications: List, create, delete OAuth2 clients
- Authentication: OIDC login via Kanidm (or dev-mode bypass)
- Single container: React frontend compiled into the Rust backend
Browser → Kanidm Admin UI (Rust + React) → Kanidm REST API
↕
OIDC (Kanidm)
- Backend: Axum (Rust) serving API routes and static files
- Frontend: React SPA compiled to
/static - Auth: OIDC via Kanidm; admin session via encrypted cookie
- Data: Kanidm is the sole source of truth (no database)
- Credentials: Service account API token stays server-side only
kanidm service account create admin_ui_svc "Admin UI Service Account"The service account needs explicit permission to manage persons, groups, and OAuth2. Kanidm uses a delegated administration model — permissions are granted via group membership.
Recommended (broad admin access):
kanidm group add_member idm_admins admin_panelScoped (principle of least privilege):
kanidm group add_member idm_person_manage admin_panel
kanidm group add_member idm_group_manage admin_panel
kanidm group add_member idm_oauth2_manage admin_panelImportant: Without these group memberships, the service account can authenticate but Kanidm will deny all search/read/create/modify operations. You'll see "denied - no entries were released" in the Kanidm logs.
kanidm service account api_token generate admin_ui_svc "admin-ui-token" --readwriteSave the generated token — it won't be shown again.
kanidm oauth2 create_basic kanidm_admin_ui "Kanidm Admin UI" \
http://localhost:8080
kanidm oauth2 update_scope_map kanidm_admin_ui idm_admin openid profile emailConfigure the admin UI with the per-client OIDC issuer:
OIDC_ISSUER_URL=https://<kanidm-origin>/oauth2/openid/kanidm_admin_ui
e.g. https://kanidm.example.com:8443/oauth2/openid/kanidm_admin_ui. The
backend validates this shape at startup — it must match Kanidm's per-client
issuer exactly, since it is checked against the iss claim of returned
id_tokens.
| Variable | Required | Description |
|---|---|---|
KANIDM_URL |
Yes | Kanidm server URL (e.g. https://kanidm.example.com:8443) |
KANIDM_API_TOKEN |
Yes | Service account API token |
KANIDM_PUBLIC_URL |
No | Browser-facing Kanidm URL for user-facing links such as password resets (defaults to KANIDM_URL) |
KANIDM_TLS_CA_FILE |
No | PEM file with a CA certificate to trust in addition to public roots (for self-signed Kanidm certs) |
EXTERNAL_URL |
Yes | Public URL of this admin UI (for OIDC callback) |
LISTEN_ADDR |
No | Listen address (default: 0.0.0.0:8080) |
COOKIE_SECRET |
No | Base64-encoded 32-byte secret for session encryption |
OIDC_ISSUER_URL |
No | Kanidm per-client OIDC issuer: https://<kanidm-origin>/oauth2/openid/<client_id> |
OIDC_CLIENT_ID |
No | OAuth2 client ID |
OIDC_CLIENT_SECRET |
No | OAuth2 client secret |
When OIDC variables are unset, the app runs in dev mode with automatic admin login.
- Rust 1.85+
- Node.js 22+
- A running Kanidm instance
# 1. Install frontend dependencies and build
cd frontend
npm install
npm run build
cd ..
# 2. Copy and configure environment
cp .env.example .env
# Edit .env with your Kanidm details
# 3. Build and run the backend
source .env
cargo runThe app will be available at http://localhost:8080.
The server listens on 0.0.0.0:8080 by default. To run on a different port:
cargo run -- --port 8081The -- separator is required — it tells cargo run to pass the remaining flags to the
binary. Without it (cargo run --port 8081), cargo rejects the flag itself.
Available flags (they override the LISTEN_ADDR environment variable):
| Flag | Description |
|---|---|
--port <PORT> |
Port to listen on (host kept from LISTEN_ADDR, default 0.0.0.0) |
--listen-addr <ADDR> |
Full listen address, e.g. 127.0.0.1:9000 |
-h, --help |
Print usage |
Alternatively, set LISTEN_ADDR=0.0.0.0:8081 (in the shell or .env).
Note: If OIDC is configured, the callback URL registered in Kanidm contains the port (
http://localhost:8080/api/auth/callback). After changing the port, update the OAuth2 client's redirect URL in Kanidm andEXTERNAL_URLto match.
For frontend development with hot reload:
# Terminal 1: backend
cargo run
# Terminal 2: frontend dev server (proxies /api to backend)
cd frontend
npm run dev# Build the image
docker build -t kanidm-admin-ui .
# Run
docker run -d \
--name kanidm-admin \
-p 8080:8080 \
-e KANIDM_URL=https://kanidm.example.com:8443 \
-e KANIDM_API_TOKEN=your_token_here \
-e EXTERNAL_URL=https://admin.example.com \
-e OIDC_ISSUER_URL=https://kanidm.example.com:8443/oauth2/openid/kanidm_admin_ui \
-e OIDC_CLIENT_ID=kanidm_admin_ui \
-e OIDC_CLIENT_SECRET=your_secret_here \
kanidm-admin-uiOr use a .env file:
docker run -d --name kanidm-admin -p 8080:8080 --env-file .env kanidm-admin-ui- Browser requests a page → backend checks for session cookie
- If no session, redirects to Kanidm OIDC login
- User authenticates with Kanidm → redirected back with auth code
- Backend exchanges code for tokens → creates encrypted session cookie
- All subsequent API calls include the session cookie
- Backend uses its service account API token for Kanidm API calls (server-side only)
- No credentials in browser: The Kanidm service account token never leaves the server
- Session encryption: Cookie values are encrypted with AES-GCM using
ring - OIDC authentication: Admins authenticate via Kanidm's OIDC flow
- Delegated admin: The backend uses a service account with appropriate Kanidm admin permissions
- Fail closed: 401/403 errors from Kanidm are propagated to the client
- No database: All data lives in Kanidm; this app is stateless
All routes are prefixed with /api:
| Method | Path | Description |
|---|---|---|
| GET | /api/auth/login |
Initiate OIDC login (or dev login) |
| GET | /api/auth/callback |
OIDC callback |
| POST | /api/auth/logout |
Clear session |
| GET | /api/auth/whoami |
Current user info |
| GET | /api/users |
List users (optional ?q=search) |
| POST | /api/users |
Create user |
| GET | /api/users/:id |
Get user details |
| DELETE | /api/users/:id |
Delete user |
| POST | /api/users/:id/disable |
Disable user |
| POST | /api/users/:id/enable |
Enable user |
| GET | /api/users/:id/groups |
Get user's groups |
| POST | /api/users/:id/groups/:group |
Add user to group |
| DELETE | /api/users/:id/groups/:group |
Remove user from group |
| GET | /api/groups |
List groups |
| POST | /api/groups |
Create group |
| GET | /api/groups/:id |
Get group details |
| DELETE | /api/groups/:id |
Delete group |
| GET | /api/groups/:id/members |
Get group members |
| POST | /api/groups/:id/members/:member |
Add member to group |
| DELETE | /api/groups/:id/members/:member |
Remove member from group |
| GET | /api/oauth2 |
List OAuth2 apps |
| POST | /api/oauth2 |
Create OAuth2 app |
| DELETE | /api/oauth2/:name |
Delete OAuth2 app |
MPL-2.0