Role-based endpoint access control for the LiteLLM proxy, layered on top of LiteLLM's native virtual-key authentication.
It lets you say things like "viewer keys may call /chat/completions but not /embeddings" without replacing LiteLLM's auth and without any changes to the LiteLLM source. Roles are stored on the key itself and enforced by a CustomLogger pre-call hook.
LiteLLM's native virtual keys already enforce model allow-lists, budgets, and expiry. What they do not give you out of the box is per-role restriction of which API endpoints a key may use. This plugin adds that, while leaving native authentication (spend tracking, budgets, revocation, model access) fully intact.
- Authentication stays native: LiteLLM validates the real virtual key against its database.
- After auth, LiteLLM invokes
async_pre_call_hookwith the resolvedUserAPIKeyAuth. - The plugin reads a
rolefrom the key'spermissionsdict (falling back tometadata), looks up the role in the role → allowed-endpoints map from yourconfig.yaml, and compares it against the request's route (UserAPIKeyAuth.request_route). - Allowed requests pass through unchanged; disallowed requests are rejected with HTTP 403.
No LiteLLM source is modified — the plugin is registered purely through config.yaml.
Install the package into the same environment as your LiteLLM proxy (for the official Docker image, see .example/docker-compose.yml, which mounts the package next to the config instead):
pip install git+https://github.com/secforge/litellm-secforge-rbac.gitRegister the plugin as a callback in your proxy config.yaml and define your roles under callback_settings:
litellm_settings:
callbacks: secforge_rbac.plugin.instance
callback_settings:
secforge_rbac:
default_role: viewer
roles:
admin: "*"
analyst: [/chat/completions, /embeddings]
viewer: [/chat/completions]Important — where the config file must live. LiteLLM resolves a callback's dotted path as a file relative to the config file's directory, not as an installed Python package. So the
secforge_rbac/package must sit next to yourconfig.yaml(i.e.<config-dir>/secforge_rbac/plugin.pymust exist). Copy, mount, or symlink thesecforge_rbac/directory beside your config (this repo's.example/does it with a symlink). See.example/config.example.yaml.
Assign a role to a key when you create it, via the key's metadata (or permissions):
curl http://localhost:4000/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"metadata": {"role": "analyst"}}'That key is now restricted to the endpoints its role allows.
Roles live in the callback_settings.secforge_rbac section of the proxy config.yaml — no plugin code needs editing:
callback_settings:
secforge_rbac:
default_role: viewer # optional; applied to keys without a role
roles: # required, non-empty
admin: "*" # "*" allows everything
viewer: [/chat/completions] # shorthand: routes only, any method, any model
analyst: # object form
routes:
- POST /chat/completions # method prefix restricts this route to POST
- GET,POST /models # multiple methods, comma-separated
- /embeddings # no prefix = any method
models: [gpt-4o-mini] # per-role model allow-list; omit or "*" = anyThree dimensions can be restricted:
- Routes — matched after stripping a leading
/v1, so/v1/embeddingsand/embeddingsare treated the same (both in requests and in the config). - HTTP methods, per route — prefix a route entry with one or more comma-separated methods (
POST /chat/completions,GET,POST /models). Unprefixed entries allow any method. Each endpoint may appear on at most one line; duplicate routes are a config error. - Models, per role — the object form's
modelslist restricts whichmodelvalues the role may request. This complements LiteLLM's native per-key model allow-lists with a role-level rule.
Keys whose role is missing fall back to default_role; if default_role is omitted, role-less keys are denied. Keys with an unknown role are denied. Method and model checks also fail closed: if a method-restricted route is hit and the request method cannot be determined, or models are restricted and the request carries none, the request is denied.
The plugin fails closed at startup too: if the callback_settings.secforge_rbac section is missing or invalid (empty roles, malformed route entries, unknown keys, a default_role not present in roles), the proxy refuses to start with a ValueError explaining what to fix — a misconfigured security layer should be loud, not silently permissive.
Every denial emits a single structured line on the secforge_rbac logger:
2026-07-20 10:46:44,160 - secforge_rbac - INFO - secforge_rbac: DENY role='viewer' route='/embeddings' -> 403 (role 'viewer' may not call '/embeddings'; allowed: ['/chat/completions'])
By default LiteLLM logs any exception raised in a pre-call hook at ERROR level with a full stack trace, so an intentional 403 would otherwise appear as a scary error. The plugin installs a narrow logging filter on the LiteLLM Proxy logger that suppresses only its own denial records (matched by a private marker), leaving just the INFO audit line above. Unrelated errors are never touched.
If you would rather keep LiteLLM's one-line ERROR for denials (for example, to surface them in an error dashboard) and only remove the traceback, change _SuppressRBACDenyTraceback.filter to strip record.exc_info and return True instead of returning False.
Local development setup, unit tests, and the end-to-end test are documented in
.test/README.md.
MIT — see LICENSE.