{"slug": "enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access", "title": "Enterprise MCP Gateway with Built-In Security: OAuth 2.0, RBAC, and Tool Access Control", "summary": "Maxim's Bifrost project introduces an enterprise MCP gateway with built-in security, including OAuth 2.0, RBAC, and tool access control. The gateway ensures that LLM tool calls are not automatically executed, requiring explicit approval, and supports deny-by-default virtual key filtering to prevent accidental production access.", "body_md": "MCP servers are powerful, but they can expose production systems if anyone on the team can connect and run tools without guardrails.\n\nImagine a new hire testing the app on their laptop and accidentally granting an MCP server access to the production database. Without governance, that is a realistic path to data leakage.\n\n[Bifrost](https://github.com/maximhq/bifrost.git) addresses this with three layers:\n\n`POST /v1/mcp/tool/execute`\n\n.`mcp_configs`\n\ngets zero MCP tools. Unlisted clients are implicitly blocked.Bifrost covers virtual keys, budgets, rate limits, routing, and MCP tool filtering, RBAC, SSO, audit logs, and MCP Tool.\n\nFirst, let's open the app and set up the MCP server. To do this, I'll enter the following line in the terminal:\n\n```\nnpx -y @maximhq/bifrost\n```\n\nAfter that, you will see the following interface (similar, depending on the version):\n\nGo to the \"MCP Library\" tab and you will see a huge list of pre-configured MCP servers that you can use in your projects.\n\nIf you want to set up your own MCP server, go to **MCP Gateway** and click **New MCP Server**:\n\nHere you can specify the connection URL, auth type, tool allowlists, and other settings including **Code Mode**, which can significantly reduce token usage when orchestrating many MCP servers.\n\nThis is the most important security property for the scenario in the introduction.\n\nWhen an LLM returns tool calls, **Bifrost does not automatically execute them**. Tool calls are suggestions only. Your application must explicitly approve and execute each one:\n\n```\n1. POST /v1/chat/completions   → LLM returns tool call suggestions (NOT executed)\n2. Your app reviews tool calls → Apply security rules, get user approval if needed\n3. POST /v1/mcp/tool/execute   → Execute approved tool calls explicitly\n4. POST /v1/chat/completions   → Continue the conversation with tool results\n```\n\nExample execution call:\n\n```\ncurl -X POST http://localhost:8080/v1/mcp/tool/execute \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"id\": \"call_xyz789\",\n    \"type\": \"function\",\n    \"function\": {\n      \"name\": \"database_query\",\n      \"arguments\": \"{\\\"sql\\\": \\\"SELECT 1\\\"}\"\n    }\n  }'\n```\n\nSo even if a new hire's agent *requests* a dangerous database operation, nothing happens until your application deliberately executes it. Combined with deny-by-default virtual key filtering (below), this is Bifrost's real three-layer answer to accidental production access.\n\nYou can opt into autonomous execution for specific tools via **Agent Mode**, but that must be explicitly configured, it is not the default.\n\nAuthentication is declared on the MCP client itself as a top-level `auth_type`\n\nfield, posted to `/api/mcp/client`\n\n. There is no nested `auth`\n\nobject.\n\n`auth_type` |\nWho authenticates | When to use |\n|---|---|---|\n`none` |\n— | Public MCP servers, local STDIO tools |\n`headers` |\nAdmin, once | Shared API keys, bearer tokens, custom headers |\n`oauth` |\nAdmin, once | Shared third-party service the whole team uses |\n`per_user_oauth` |\nEach end-user, lazily | Per-user services like Notion, GitHub, Sentry |\n`per_user_headers` |\nEach end-user, lazily | Per-user API keys, signed tokens |\n\nOAuth (`oauth`\n\nand `per_user_oauth`\n\n) is only valid for **HTTP** and **SSE** connections. Bifrost implements the **Authorization Code** flow, there is no client-credentials / service-account mode.\n\n```\n{\n  \"name\": \"local-tools\",\n  \"connection_type\": \"stdio\",\n  \"stdio_config\": {\n    \"command\": \"npx\",\n    \"args\": [\"-y\", \"@anthropic/mcp-filesystem\"]\n  },\n  \"auth_type\": \"none\",\n  \"tools_to_execute\": [\"read_file\", \"list_directory\"]\n}\ncurl -X POST http://localhost:8080/api/mcp/client \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"web_search\",\n    \"connection_type\": \"http\",\n    \"connection_string\": \"https://mcp.example.com/mcp\",\n    \"auth_type\": \"headers\",\n    \"headers\": {\n      \"Authorization\": \"Bearer your-api-key\",\n      \"X-Tenant-ID\": \"acme-corp\"\n    },\n    \"tools_to_execute\": [\"*\"]\n  }'\n```\n\nThe admin authenticates once during setup. Every subsequent request to that MCP server uses the same stored token, regardless of which caller hit Bifrost.\n\n```\ncurl -X POST http://localhost:8080/api/mcp/client \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"authenticated_service\",\n    \"connection_type\": \"http\",\n    \"connection_string\": \"https://api.example.com/mcp\",\n    \"auth_type\": \"oauth\",\n    \"oauth_config\": {\n      \"client_id\": \"your-client-id\",\n      \"client_secret\": \"your-client-secret\",\n      \"authorize_url\": \"https://auth.example.com/oauth/authorize\",\n      \"token_url\": \"https://auth.example.com/oauth/token\",\n      \"scopes\": [\"mcp:read\", \"mcp:write\"]\n    },\n    \"tools_to_execute\": [\"*\"]\n  }'\n```\n\nThe `oauth_config`\n\nobject accepts `client_id`\n\n, `client_secret`\n\n, `authorize_url`\n\n, `token_url`\n\n, `scopes`\n\n, or `registration_url`\n\n/ `server_url`\n\nfor Dynamic Client Registration. After the admin completes the authorize step, finalize with `POST /api/mcp/client/{id}/complete-oauth`\n\n.\n\nUse `auth_type: \"per_user_oauth\"`\n\nwhen each end-user must connect under their own account. Bifrost stores one OAuth token per `(identity, mcp_client)`\n\nand reuses it on later calls. Identity is required via virtual key, signed-in SSO user, or `x-bf-mcp-session-id`\n\n.\n\n```\ncurl -X POST http://localhost:8080/api/mcp/client \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"acme_api\",\n    \"connection_type\": \"http\",\n    \"connection_string\": \"https://api.acme.example.com/mcp\",\n    \"auth_type\": \"per_user_headers\",\n    \"per_user_header_keys\": [\"X-API-Key\", \"X-Tenant-ID\"],\n    \"tools_to_execute\": [\"*\"]\n  }'\n```\n\n**Identity matters:** With `auth_type: \"oauth\"`\n\nor `auth_type: \"headers\"`\n\n, all callers share the same upstream credential. Bifrost does not attach a per-user identity to MCP requests. To know exactly *who* performed an action upstream, use `per_user_oauth`\n\nor `per_user_headers`\n\n.\n\nRBAC does **not** govern which MCP tools an agent can invoke at runtime. That is controlled by **virtual keys** and three stacked levels of tool filtering:\n\n`tools_to_execute`\n\non each MCP client (baseline)`x-bf-mcp-include-clients`\n\nand `x-bf-mcp-include-tools`\n\nper request`mcp_configs`\n\narray (takes precedence over request headers)This is built-in behavior, not a config setting: **a virtual key with no mcp_configs gets zero MCP tools**, and clients not listed in\n\n`mcp_configs`\n\nare implicitly blocked.\n\n```\ncurl -X POST http://localhost:8080/api/governance/virtual-keys \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"new-dev-key\",\n    \"mcp_configs\": [\n      {\n        \"mcp_client_name\": \"internal_api\",\n        \"tools_to_execute\": [\"search\", \"get_article\"]\n      },\n      {\n        \"mcp_client_name\": \"staging_database\",\n        \"tools_to_execute\": [\"query\"]\n      }\n    ]\n  }'\n```\n\n`tools_to_execute` |\nResult |\n|---|---|\n`[\"*\"]` |\nAll tools from this client |\n`[\"a\", \"b\"]` |\nOnly specified tools |\n`[]` |\nNo tools from this client |\nClient not in `mcp_configs`\n|\nAll tools blocked from that client |\n\nThis is where you enforce patterns like \"backend devs can hit staging APIs but not production databases\" by giving different virtual keys different `mcp_configs`\n\n, not by RBAC permission strings.\n\nFor one-off restrictions within a virtual key's allowlist:\n\n```\ncurl -X POST http://localhost:8080/v1/chat/completions \\\n  -H \"Authorization: Bearer vk_new_dev\" \\\n  -H \"x-bf-mcp-include-tools: staging_database-query\" \\\n  -d '...'\n```\n\nNote: when a virtual key has `mcp_configs`\n\n, it auto-generates `x-bf-mcp-include-tools`\n\nand overrides any manually sent header.\n\nBifrost does not parse SQL or block operations like `DELETE`\n\n/ `DROP`\n\nat the query level. Restrict access by allowing only specific tool names (for example, a read-only `query`\n\ntool instead of an `execute`\n\ntool).\n\n**Bifrost** provides Role-Based Access Control for the **administrative surface** who can edit MCP gateway configs, read logs, configure guardrails, manage virtual keys, and so on. RBAC is **not** runtime authorization for agents invoking MCP tools.\n\nPermissions are **Resource × Operation** pairs, not permission strings like `mcp:tool:invoke`\n\n.\n\n| Role | Permissions | Description |\n|---|---|---|\nAdmin |\n42 | Full access to all resources and operations |\nDeveloper |\n27 | CRUD on technical resources, view access to logs and cluster |\nViewer |\n14 | Read-only access to all resources |\n\nYou can also create custom roles (for example, an Auditor role with `AuditLogs:View`\n\nand `Logs:View`\n\nonly).\n\n`Logs`\n\n, `VirtualKeys`\n\n, `MCPGateway`\n\n, `MCPToolGroups`\n\n, `MCPLogs`\n\n, `GuardrailsConfig`\n\n, `AuditLogs`\n\n, `Cluster`\n\n, and others.\n\n`View`\n\n, `Create`\n\n, `Update`\n\n, `Delete`\n\n, `Download`\n\n, `Reveal`\n\n, and inference operations.\n\nExample: a custom Auditor role might grant `AuditLogs:View`\n\nand `AuditLogs:Download`\n\n, but not `MCPGateway:Update`\n\n. That controls who can *configure* the gateway in the dashboard, not which tools an agent executes at runtime.\n\nRoles and permissions are managed via **Governance → Roles & Permissions** in the dashboard or the `/api/roles`\n\nendpoints:\n\n```\ncurl -X GET http://localhost:8080/api/roles/{role_id}/permissions \\\n  -H \"Authorization: Bearer <admin_token>\"\n```\n\nThere is no `role_sync`\n\nconfig block. Role assignment comes from **User Provisioning over OIDC**, supported for Okta, Microsoft Entra and others.\n\nWhen SSO is configured:\n\nConfiguration lives under `scim_config`\n\nin `config.json`\n\n. See the [User Provisioning docs](https://docs.getbifrost.ai/enterprise/user-provisioning) for provider-specific setup guides.\n\nAudit logs in Bifrost record **administrative activity** who changed what, when, and which resource was affected. They do not use a `log_level`\n\n/ `capture`\n\n/ `export_to`\n\nblock.\n\nReal configuration shape:\n\n```\n{\n  \"audit_logs\": {\n    \"disabled\": false,\n    \"hmac_key\": \"env.AUDIT_HMAC_KEY\",\n    \"retention_days\": 365,\n    \"object_storage\": {\n      \"type\": \"s3\",\n      \"bucket\": \"acme-audit-archive\",\n      \"prefix\": \"acme-prod\",\n      \"compress\": true,\n      \"region\": \"us-east-1\",\n      \"access_key_id\": \"env.AUDIT_S3_KEY\",\n      \"secret_access_key\": \"env.AUDIT_S3_SECRET\"\n    }\n  }\n}\n```\n\nKey features:\n\n`AuditLogs:Download`\n\npermission)`retention_days`\n\ncontrols database retentionView audit entries at **Governance → Audit Logs** in the dashboard.\n\nDo not look for a `\"policy\": \"default_deny\"`\n\nsetting. It does not exist. Instead:\n\n`mcp_configs`\n\nfor each team or environment`tools_to_execute`\n\nto the minimum neededOnly enable [Agent Mode](https://docs.getbifrost.ai/mcp/agent-mode) auto-execution for tools you have explicitly reviewed. The default flow — chat → review → `/v1/mcp/tool/execute`\n\n— is your strongest safety net.\n\n`mcp_configs`\n\n+ request headers\n\n```\n{\n  \"name\": \"production-readonly\",\n  \"mcp_configs\": [\n    { \"mcp_client_name\": \"production_database\", \"tools_to_execute\": [\"query\"] }\n  ]\n}\n{\n  \"name\": \"staging-full\",\n  \"mcp_configs\": [\n    { \"mcp_client_name\": \"staging_database\", \"tools_to_execute\": [\"*\"] }\n  ]\n}\n```\n\nEnable HMAC signing, set `retention_days`\n\ncomfortably above your archival window, and optionally mirror to object storage for compliance.\n\nSchedule quarterly reviews to answer:\n\nUse the dashboard and `/api/roles`\n\nendpoints, there is no `bifrost audit`\n\nCLI command. The `@maximhq/bifrost-cli`\n\npackage is an interactive launcher for coding agents (Claude Code, Codex CLI, Gemini CLI, Opencode), not an audit tool.\n\nWith Bifrost, you can configure your company's MCP server much more securely. This ready-made solution will save you not only money but also time, which can be spent on product development.\n\n`npx -y @maximhq/bifrost-cli`\n\n**Thanks for reading this article! ❤️**\n\n*I'd love to hear your thoughts on this mode in the comments!*", "url": "https://wpnews.pro/news/enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access", "canonical_source": "https://dev.to/anthonymax/enterprise-mcp-gateway-with-built-in-security-oauth-20-rbac-and-tool-access-control-68n", "published_at": "2026-08-05 19:41:55+00:00", "updated_at": "2026-08-05 19:58:29.390988+00:00", "lang": "en", "topics": ["ai-infrastructure", "ai-safety", "developer-tools", "ai-agents"], "entities": ["Bifrost", "Maxim", "MCP", "OAuth 2.0", "RBAC"], "alternates": {"html": "https://wpnews.pro/news/enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access", "markdown": "https://wpnews.pro/news/enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access.md", "text": "https://wpnews.pro/news/enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access.txt", "jsonld": "https://wpnews.pro/news/enterprise-mcp-gateway-with-built-in-security-oauth-2-0-rbac-and-tool-access.jsonld"}}