RBAC ConfigurationΒΆ
Role-based access control (RBAC) defines which actions users or teams can perform in MCP Gateway. This document covers the two-layer security model, token scoping semantics, permission system, and best practices for access control.
OverviewΒΆ
MCP Gateway implements a two-layer security model:
- Token Scoping (Layer 1): Controls what resources a user CAN SEE (data filtering)
- RBAC (Layer 2): Controls what actions a user CAN DO (action authorization)
Both layers must pass for an operation to succeed.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Two-Layer Security Model β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Request β Authentication β Token Scoping β RBAC Check β Operation β
β (Can See?) (Can Do?) β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β JWT β β User β β Resource β βPermissionβ β Execute β β
β β Token ββββΆβ Identity ββββΆβ Access ββββΆβ Check ββββΆβ Operationβ β
β β β β β β β β β β β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Authentication MethodsΒΆ
| Method | Priority | Description |
|---|---|---|
| JWT Token | 1 (Primary) | Signature verified, supports teams/scopes claims |
| Plugin Auth | 0 (Before JWT) | HTTP_AUTH_RESOLVE_USER hook can provide custom auth |
| API Token (DB) | 2 (Fallback) | Legacy database-stored tokens |
| Proxy Header | 3 | When MCP_CLIENT_AUTH_ENABLED=false AND TRUST_PROXY_AUTH=true |
| Anonymous | 4 | When AUTH_REQUIRED=false (development only) |
Core ConceptsΒΆ
SubjectsΒΆ
Users authenticated via:
- JWT tokens (session or API)
- SSO providers (OAuth 2.0/OIDC)
- Basic authentication (development only)
TeamsΒΆ
Logical groups that:
- Organize users for access boundaries
- Own resources (tools, prompts, resources)
- Map from external identity providers (SSO groups)
Built-in RBAC RolesΒΆ
| Role | Scope | Permissions |
|---|---|---|
platform_admin | global | ["*"] (all permissions) |
team_admin | team | admin.dashboard, gateways.read, gateways.create, gateways.update, gateways.delete, servers.read, servers.create, servers.update, servers.delete, teams.read, teams.update, teams.join, teams.delete, teams.manage_members, tools.read, tools.create, tools.update, tools.delete, tools.execute, resources.read, resources.create, resources.update, resources.delete, prompts.read, prompts.create, prompts.update, prompts.delete, a2a.read, a2a.create, a2a.update, a2a.delete, a2a.invoke |
developer | team | admin.dashboard, gateways.read, gateways.create, gateways.update, gateways.delete, servers.read, servers.create, servers.update, servers.delete, teams.join, tools.read, tools.create, tools.update, tools.delete, tools.execute, resources.read, resources.create, resources.update, resources.delete, prompts.read, prompts.create, prompts.update, prompts.delete, a2a.read, a2a.create, a2a.update, a2a.delete, a2a.invoke |
viewer | team | admin.dashboard, gateways.read, servers.read, teams.join, tools.read, resources.read, prompts.read, a2a.read |
platform_viewer | global | admin.dashboard, gateways.read, servers.read, teams.join, tools.read, resources.read, prompts.read, a2a.read |
Default Role Assignment
New users automatically receive up to two roles upon creation:
Admin users (is_admin: true) receive:
platform_adminrole with global scope (scope_id= None)- Grants unrestricted access to all platform resources
team_adminrole with team scope (scope_id= personal team ID)- Grants full management of their personal team resources
- Only assigned if personal team creation succeeds
Non-admin users (is_admin: false) receive:
platform_viewerrole with global scope (scope_id= None)- Grants read-only access to all platform resources
team_adminrole with team scope (scope_id= personal team ID)- Grants full management of their personal team resources
- Only assigned if personal team creation succeeds
This dual-role approach ensures: - Users always have appropriate global visibility (via platform_admin or platform_viewer) - Users can fully manage their personal team resources (via team-scoped team_admin, when available) - Clear separation between team-level and platform-level permissions
The granted_by field tracks which admin created the user for audit purposes.
Existing Users Migration
An Alembic migration (v1a2b3c4d5e6) automatically updates existing users without roles:
Previous behavior (before migration): - Admin users: Only platform_admin with global scope (scope_id = None) - Non-admin users: Only viewer with team scope (scope_id = None)
After migration:
Admin users receive:
team_adminrole with team scopescope_id= user's personal team ID (fromemail_team_memberstable)- Enables management of personal team resources
platform_adminrole with global scopescope_id= None- Maintains unrestricted platform access
Non-admin users receive:
team_adminrole with team scopescope_id= user's personal team ID (fromemail_team_memberstable)- Enables management of personal team resources
platform_viewerrole with global scopescope_id= None- Provides read-only access to platform resources
Migration behavior: - Only affects users without existing role assignments - Users without a personal team still receive their global role (team_admin is skipped) - The platform admin (configured via PLATFORM_ADMIN_EMAIL) is excluded to preserve bootstrap configuration - Migration is idempotent and safe to run multiple times
ResourcesΒΆ
Protected entities:
- Servers (MCP gateways and virtual servers)
- Tools, Prompts, Resources (MCP primitives)
- System configuration and audit logs
Token Scoping ModelΒΆ
Token scoping controls what resources a token can access based on the teams claim in the JWT payload. The normalize_token_teams() function is the single source of truth for interpreting JWT team claims across all enforcement points.
Token Scoping ContractΒΆ
The teams claim in JWT tokens determines resource visibility. The system follows a secure-first design: when in doubt, access is denied.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Token Teams Claim Handling β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β JWT Claim State β is_admin: true β is_admin: false β
βββββββββββββββββββββββββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββββ€
β No "teams" key β PUBLIC-ONLY [] β PUBLIC-ONLY [] β
β teams: null β ADMIN BYPASS (None) β PUBLIC-ONLY [] β
β teams: [] β PUBLIC-ONLY [] β PUBLIC-ONLY [] β
β teams: ["team-id"] β Team + Public β Team + Public β
β teams: ["t1", "t2"] β Both Teams + Public β Both Teams + Public β
βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββ΄ββββββββββββββββββββββββββ
Admin Bypass Requirements
Admin bypass (unrestricted access) requires BOTH conditions:
teams: null(explicit null, not missing key)is_admin: true
A missing teams key always results in public-only access, even for admins. An empty teams: [] also results in public-only access, even for admins.
Return Value SemanticsΒΆ
| Return Value | Meaning | Query Behavior |
|---|---|---|
None | Admin bypass | Skip ALL team filtering |
[] (empty list) | Public-only | Filter to visibility='public' ONLY |
["t1", "t2"] | Team-scoped | Filter to team resources + public |
Security Design PrinciplesΒΆ
-
Secure-First Defaults
-
Missing
teamskey always returns[](public-only access) -
This prevents accidental exposure when tokens are misconfigured
-
Explicit Admin Bypass
-
Admin bypass requires explicit
teams: nullANDis_admin: true -
Empty teams
[]disables bypass even for admins -
Scoped Automation Tokens
-
Tokens with
teams: []are intentionally restricted to public resources - Use case: CI/CD pipelines, monitoring systems, public API clients
Token Scoping FlowΒΆ
ββββββββββββββββββββ
β JWT Token β
β Received β
ββββββββββ¬ββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Extract "teams" β
β claim from JWT β
βββββββββββββ¬ββββββββββββ
β
βββββββββββββββββ΄ββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β "teams" key EXISTS β β "teams" key MISSING β
ββββββββββββ¬βββββββββββ ββββββββββββ¬βββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β Check teams value β β Return [] β
ββββββββββββ¬βββββββββββ β PUBLIC-ONLY β
β β (secure default) β
βββββββββββββββββΌββββββββββββββββ βββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββ βββββββββββββ βββββββββββββββββ
β teams: null β β teams: [] β β teams: [...] β
βββββββββ¬ββββββββ βββββββ¬ββββββ βββββββββ¬ββββββββ
β β β
βΌ β βΌ
βββββββββββββββββ β βββββββββββββββββ
β Check is_adminβ β β Return [...] β
βββββββββ¬ββββββββ β β TEAM-SCOPED β
β β βββββββββββββββββ
βββββββ΄ββββββ β
β β β
βΌ βΌ βΌ
ββββββββββ ββββββββββ ββββββββββ
β Admin β βNon-Adm β β Empty β
β true β β false β β list β
ββββββββββ€ ββββββββββ€ ββββββββββ€
β Return β β Return β β Return β
β None β β [] β β [] β
β BYPASS β β PUBLIC β β PUBLIC β
ββββββββββ ββββββββββ ββββββββββ
Key Insight
The difference between teams: null and missing teams key is critical:
- Missing key: Always
[](public-only) - secure default - Explicit null: Admin bypass when
is_admin: true, otherwise[]
Visibility LevelsΒΆ
Resources in MCP Gateway have three visibility levels:
| Visibility | Description | Who Can See |
|---|---|---|
public | Accessible to all authenticated users | Everyone with valid token |
team | Accessible to team members only | Team members + admins (with bypass) |
private | Accessible to owner only | Resource owner + admins (with bypass) |
Access Matrix by Token TypeΒΆ
| Token Type | Public Resources | Team Resources | Private Resources |
|---|---|---|---|
Admin Bypass (teams=null, is_admin=true) | β | β (all teams) | β (all) |
Team-Scoped (teams=["t1"]) | β | β (own team) | β (own only) |
Public-Only (teams=[]) | β | β | β |
Public-Only Token Limitations
Public-only tokens (teams=[]) cannot access private resources, even if the resource is owned by the token's user.
This is intentional security behavior - public-only tokens are designed for limited-scope access to public resources only. To access private resources, users must use a team-scoped token that includes their personal team.
Enforcement PointsΒΆ
Token scoping is enforced consistently across all access paths:
| Location | Token Scoping | RBAC | Description |
|---|---|---|---|
| Token Scoping Middleware | β | N/A | Request-level data filtering |
| REST API Endpoints | β | β | @require_permission decorators |
RPC Handler (/rpc) | β | Varies | Method-specific permission checks |
| Admin UI | β | β | Permission-based UI rendering |
| Service Layer | β | N/A | Database query filtering |
| WebSocket | β | β | Forwards auth to /rpc |
| MCP Transport | β | N/A | Streamable HTTP protocol filtering |
Token Types and Use CasesΒΆ
Session Tokens (UI Login)ΒΆ
Generated when users log in via the Admin UI:
{
"sub": "admin@example.com",
"is_admin": true,
"teams": null,
"iss": "mcpgateway",
"aud": "mcpgateway-api",
"exp": 1234567890
}
Behavior: Admin session tokens should set teams: null (explicit null) combined with is_admin: true to enable admin bypass (unrestricted access to all resources).
API Tokens (Programmatic Access)ΒΆ
Generated via the Admin UI or API for automation:
{
"sub": "service-account@example.com",
"is_admin": false,
"teams": ["team-uuid-1", "team-uuid-2"],
"iss": "mcpgateway",
"aud": "mcpgateway-api",
"exp": 1234567890
}
Behavior: Access restricted to public resources plus resources owned by specified teams.
Scoped Automation TokensΒΆ
For CI/CD, monitoring, or public API access:
{
"sub": "ci-pipeline@example.com",
"is_admin": true,
"teams": [], // Explicitly empty = public-only
"iss": "mcpgateway",
"aud": "mcpgateway-api",
"exp": 1234567890
}
Behavior: Even admin tokens with teams: [] are restricted to public resources only. This enables creating limited-scope tokens for automation that shouldn't access team-internal resources.
Generating Scoped TokensΒΆ
Using the CLI ToolΒΆ
# Unrestricted admin token (no teams key)
python3 -m mcpgateway.utils.create_jwt_token \
--username admin@example.com \
--exp 60 \
--secret $JWT_SECRET_KEY
# Team-scoped token
python3 -m mcpgateway.utils.create_jwt_token \
--username user@example.com \
--exp 60 \
--secret $JWT_SECRET_KEY \
--teams '["team-uuid-1"]'
# Public-only scoped token (for automation)
python3 -m mcpgateway.utils.create_jwt_token \
--username ci@example.com \
--exp 60 \
--secret $JWT_SECRET_KEY \
--teams '[]'
Using the Admin UIΒΆ
- Navigate to Admin UI β Tokens
- Click Create Token
-
Select team scope:
-
No team selected: Public resources only (secure default)
-
Specific team(s): Team + public resources
-
Configure additional restrictions (IP, permissions, expiry)
Token Scope Warning
Tokens created without selecting a team will have access to public resources only. This is the secure default to prevent accidental exposure of team resources.
Permission SystemΒΆ
Permission CategoriesΒΆ
Permissions are defined in the Permissions class and control what actions users can perform:
| Category | Permissions |
|---|---|
| Users | users.create, users.read, users.update, users.delete, users.invite |
| Teams | teams.create, teams.read, teams.update, teams.delete, teams.join, teams.manage_members |
| Tools | tools.create, tools.read, tools.update, tools.delete, tools.execute |
| Resources | resources.create, resources.read, resources.update, resources.delete, resources.share |
| Gateways | gateways.create, gateways.read, gateways.update, gateways.delete |
| Prompts | prompts.create, prompts.read, prompts.update, prompts.delete, prompts.execute |
| Servers | servers.create, servers.read, servers.update, servers.delete, servers.manage |
| Tokens | tokens.create, tokens.read, tokens.update, tokens.revoke |
| Admin | admin.system_config, admin.user_management, admin.security_audit, admin.overview, admin.dashboard, admin.events, admin.grpc, admin.plugins |
| A2A | a2a.create, a2a.read, a2a.update, a2a.delete, a2a.invoke |
| Tags | tags.read, tags.create, tags.update, tags.delete |
| Wildcard | * (all permissions) |
Permission Checking FlowΒΆ
@require_permission("resource.action")
β
βΌ
βββββββββββββββββββββββββββββββ
β Extract user_context β β From request/kwargs
βββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β Plugin Permission Hook β β HTTP_AUTH_CHECK_PERMISSION can override
βββββββββββββββββββββββββββββββ
β (no plugin decision)
βΌ
βββββββββββββββββββββββββββββββ
β Admin Bypass Check β β If allow_admin_bypass=True AND user.is_admin
βββββββββββββββββββββββββββββββ
β (not admin or bypass disabled)
βΌ
βββββββββββββββββββββββββββββββ
β Role Collection β β Get all active roles for user
β - Global scope roles β
β - Personal scope roles β
β - Team scope roles β
βββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β Permission Aggregation β β Collect permissions from roles
β - Include inherited perms β (role inheritance supported)
β - Check for wildcard (*) β
βββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β Fallback Permission Check β β Implicit permissions (see below)
βββββββββββββββββββββββββββββββ
β
βΌ
GRANT or DENY
Fallback PermissionsΒΆ
The system grants implicit permissions without explicit role assignment. These are not shown in /rbac/my/permissions but are effective:
| User Context | Implicit Permissions |
|---|---|
| Any authenticated user | teams.create, teams.read |
| Team member | teams.read (for their teams) |
| Team owner | teams.read, teams.update, teams.delete, teams.manage_members |
| Any authenticated user | tokens.* (for own tokens only) |
Why Fallback Permissions Exist
Fallback permissions enable basic functionality without requiring explicit role assignment:
- Users can always create and view teams they belong to
- Team owners automatically have management rights
- Users can always manage their own API tokens
Admin API RBACΒΆ
The Admin API enforces strict RBAC where even users with is_admin: true must have explicit permissions granted. This enables delegated administration - granting specific admin capabilities without full superuser access.
Key behaviors:
| Aspect | Behavior |
|---|---|
| Admin bypass | allow_admin_bypass=False on all admin routes |
is_admin flag | Does NOT bypass permission checks |
| UI entry | Requires any admin.* permission via has_admin_permission() |
| Route protection | All 177 admin routes use @require_permission decorators |
Example: Delegated Server Management
{
"role": "server-manager",
"permissions": [
"servers.read",
"servers.create",
"servers.update",
"servers.delete"
]
}
A user with this role can:
- β
Access
/admin/servers/*endpoints - β
View the Admin UI (has
servers.*which satisfieshas_admin_permission()) - β Access
/admin/tools/*endpoints (notools.*permissions) - β Access
/admin/gateways/*endpoints (nogateways.*permissions)
Platform Admin Role
The built-in platform_admin role has ["*"] (wildcard) permissions, which grants access to all operations. For delegated administration, create custom roles with specific permission sets.
Configuration SafetyΒΆ
Development vs Production SettingsΒΆ
The following configuration combinations require careful consideration:
| Setting | Value | Impact | Recommended Use |
|---|---|---|---|
AUTH_REQUIRED | false | All requests granted admin access | Development only |
TRUST_PROXY_AUTH | true + MCP_CLIENT_AUTH_ENABLED=false | Trust X-Forwarded-User header without verification | Behind trusted reverse proxy only |
Proxy Authentication ModeΒΆ
When MCP_CLIENT_AUTH_ENABLED=false and TRUST_PROXY_AUTH=true:
- The gateway trusts the
X-Forwarded-Userheader from upstream proxy - No JWT validation or database verification is performed
- Only use when deployed behind a trusted reverse proxy that handles authentication
Security Warning
Proxy authentication mode should only be used in trusted network environments where the reverse proxy is the only entry point to the gateway. Exposing the gateway directly to untrusted networks with this configuration allows header injection attacks.
Anonymous Mode (AUTH_REQUIRED=false)ΒΆ
When AUTH_REQUIRED=false:
- All unauthenticated requests receive platform-admin context
- Never use in production - all users have full admin access
- Intended only for local development and testing
Production Warning
Setting AUTH_REQUIRED=false in production grants administrative access to all requests. This completely bypasses authentication and authorization.
Best PracticesΒΆ
Token LifecycleΒΆ
- Use short expiration times for interactive sessions (hours)
- Use longer expiration for service accounts with IP restrictions
- Rotate tokens regularly (recommended: 90 days for long-lived tokens)
- Revoke tokens immediately when access should be removed
Team OrganizationΒΆ
-
Create purpose-specific teams:
-
platform-admins- Full administrative access developers- Development and testing resourcesci-automation- CI/CD pipeline access-
monitoring- Read-only observability access -
Map SSO groups to teams for automatic membership management
- Use personal teams for individual resource ownership
Scoping StrategyΒΆ
| Use Case | Recommended Token Scope |
|---|---|
| Admin UI access | Session token (teams: null + is_admin: true) |
| CI/CD pipeline | teams: [] (public-only) |
| Service integration | Specific team(s) |
| Developer access | Personal team + project teams |
| Monitoring/alerting | teams: [] with read permissions |
TroubleshootingΒΆ
Token Not Seeing Expected ResourcesΒΆ
-
Check token claims: Decode the JWT to verify
teamsclaim -
Verify resource visibility: Check the resource's
visibilityandteam_id -
Check user admin status: Non-admin users without teams get public-only access
Admin Token Being RestrictedΒΆ
If an admin token is unexpectedly restricted:
- Check for explicit
teamsclaim:teams: []restricts even admins - Verify
is_adminflag: Must betruein JWT or database user - Check middleware logs: Look for "token_teams" in debug output
Inconsistent Results Between EndpointsΒΆ
If REST and RPC endpoints return different results:
- Check for caching: REST list endpoints may have cached data
- Wait for cache TTL: Default is 60 seconds for registry cache
- Use direct GET:
/tools/{id}bypasses list cache
Bootstrap Custom RolesΒΆ
MCP Gateway allows you to define custom roles that are automatically created during database bootstrap. This is useful for organizations that need to pre-configure roles before deployment.
ConfigurationΒΆ
Enable custom role bootstrapping with these environment variables:
| Variable | Default | Description |
|---|---|---|
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED | false | Enable loading additional roles from file |
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE | additional_roles_in_db.json | Path to the JSON file containing role definitions |
Role Definition FormatΒΆ
Create a JSON file containing an array of role definitions:
[
{
"name": "data_analyst",
"description": "Read-only access for data analysis",
"scope": "team",
"permissions": ["tools.read", "resources.read", "prompts.read"],
"is_system_role": true
},
{
"name": "auditor",
"description": "Compliance audit access",
"scope": "global",
"permissions": ["tools.read", "resources.read", "prompts.read", "servers.read", "gateways.read"],
"is_system_role": true
}
]
Required fields:
name- Unique role namescope- Eitherteam(team-level access) orglobal(system-wide access)permissions- Array of permission strings (e.g.,tools.read,resources.create)
Optional fields:
description- Human-readable descriptionis_system_role- Set totrueto prevent users from modifying/deleting the role
Available PermissionsΒΆ
| Resource | Permissions |
|---|---|
| Tools | tools.create, tools.read, tools.update, tools.delete, tools.execute |
| Resources | resources.create, resources.read, resources.update, resources.delete |
| Prompts | prompts.create, prompts.read, prompts.update, prompts.delete |
| Servers | servers.create, servers.read, servers.update, servers.delete |
| Gateways | gateways.create, gateways.read, gateways.update, gateways.delete |
| Teams | teams.create, teams.read, teams.update, teams.delete, teams.join |
Docker Compose ExampleΒΆ
services:
gateway:
environment:
- MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED=true
- MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE=/app/custom_roles.json
volumes:
- ./custom_roles.json:/app/custom_roles.json:ro
Kubernetes/Helm ExampleΒΆ
# values.yaml
mcpContextForge:
env:
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_ENABLED: "true"
MCPGATEWAY_BOOTSTRAP_ROLES_IN_DB_FILE: "/config/custom_roles.json"
# Mount ConfigMap with role definitions
extraVolumes:
- name: custom-roles
configMap:
name: mcp-gateway-roles
extraVolumeMounts:
- name: custom-roles
mountPath: /config
Error HandlingΒΆ
- File not found: Bootstrap continues with default roles only; warning logged
- Invalid JSON: Bootstrap continues with default roles only; error logged
- Malformed entries: Invalid role entries are skipped with warnings; valid entries are processed
Idempotent Bootstrap
Bootstrap is idempotent - running it multiple times won't duplicate roles. Existing roles are detected and skipped.
Related DocumentationΒΆ
- Team Management - Setting up teams and SSO mapping
- Security Features - Comprehensive security configuration
- Configuration Reference - Environment variables
- API Usage - Token usage in API calls