Skip to main content
Redhat Developers  Logo
  • Products

    Platforms

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat AI
      Red Hat AI
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • See all Red Hat products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat Developer Hub
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat OpenShift Local
    • Red Hat Developer Sandbox

      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Red Hat OpenShift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • See all technologies
    • Programming languages & frameworks

      • Java
      • Python
      • JavaScript
    • System design & architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer experience

      • Productivity
      • Tools
      • GitOps
    • Automated data processing

      • AI/ML
      • Data science
      • Apache Kafka on Kubernetes
    • Platform engineering

      • DevOps
      • DevSecOps
      • Red Hat Ansible Automation Platform for applications and services
    • Secure development & architectures

      • Security
      • Secure coding
  • Learn

    Featured

    • Kubernetes & cloud native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • AI/ML
      AI/ML Icon
    • See all learning resources

    E-books

    • GitOps cookbook
    • Podman in action
    • Kubernetes operators
    • The path to GitOps
    • See all e-books

    Cheat sheets

    • Linux commands
    • Bash commands
    • Git
    • systemd commands
    • See all cheat sheets

    Documentation

    • Product documentation
    • API catalog
    • Legacy documentation
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore the Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Advanced authentication and authorization for MCP Gateway

Leveraging Kubernetes Gateway policies for enterprise-grade AI agent tool access

December 12, 2025
Guilherme Cassolato
Related topics:
Artificial intelligenceSecurity
Related products:
Red Hat AIRed Hat OpenShift AIRed Hat Connectivity Link

    The MCP Gateway is an Envoy-based gateway that aggregates multiple Model Context Protocol (MCP) servers behind a single endpoint, enabling AI agents to access tools from diverse sources through a unified interface. While this aggregation simplifies the client experience, it introduces critical security challenges: How do you control which users can access which tools across multiple MCP servers? How do you prevent overly permissive access tokens from being leaked to untrusted servers? And how do you handle MCP servers that use different authentication mechanisms?

    This post explores three advanced authentication and authorization capabilities we've implemented for the MCP Gateway, using Kuadrant as an add-on to provide enterprise-grade security without creating hard dependencies.

    The security challenge

    When an MCP Gateway aggregates tools from multiple servers, several security challenges emerge:

    • Overly permissive tokens: AI agents typically use OAuth2 access tokens with broad scopes covering all tools across all MCP servers. If these tokens are passed directly to backend servers, a compromised or malicious MCP server could use them to impersonate the agent and access unauthorized resources.
    • Coarse-grained access control: The standard MCP protocol doesn't provide a mechanism for filtering which tools are visible based on user identity. An agent sees all available tools, even if the user isn't authorized to use some of them. This problem is exacerbated when the MCP Broker component of the MCP Gateway aggregates tools from multiple MCP servers, including servers possibly unrelated to user's group associations and roles.
    • Heterogeneous authentication: Different MCP servers might require different authentication methods—some use OAuth2, others use personal access tokens (PATs), and some use API keys. The gateway needs to translate between these mechanisms transparently.

    Solution overview

    We've addressed these challenges through three complementary capabilities:

    • Identity-based tool filtering: Filter the tools/list response based on user permissions using a cryptographically signed header.
    • OAuth2 Token Exchange: Use RFC 8693 to exchange broad access tokens for narrowly-scoped tokens specific to each MCP server.
    • Vault integration: Retrieve PATs and API keys from HashiCorp Vault for servers that don't support OAuth2.

    All three capabilities are implemented using Kuadrant's AuthPolicy resource, which integrates with the MCP Gateway's Envoy-based architecture. However, the gateway itself remains agnostic to the authentication mechanism—you could implement these patterns using any Istio/Gateway API compatible policy engine.

    Watch a demo of the full solution in action:

    1. Identity-based tool filtering

    The problem: When an agent calls tools/list, the MCP Gateway returns all tools from all registered MCP servers. But what if the user is only authorized to access a subset of these tools? Showing unauthorized tools creates confusion and potential security issues.

    The solution: We use a trusted header approach with cryptographic verification:

    1. An external authorization component (Authorino in our case) validates the user's OAuth2 token and extracts their permissions from the identity provider.
    2. It creates a signed JWT "wristband" that holds the permitted tools and injects it as the x-authorized-tools header.
    3. The MCP Broker validates this JWT using a trusted public key and filters the tool list accordingly.

    Here's the AuthPolicy configuration:

    apiVersion: kuadrant.io/v1
    kind: AuthPolicy
    metadata:
      name: mcp-auth-policy
      namespace: gateway-system
    spec:
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: mcp-gateway
        sectionName: mcp
      when:
        - predicate: "!request.path.contains('/.well-known')"
      rules:
        authentication:
          'keycloak':
            jwt:
              issuerUrl: https://keycloak.example.com/realms/mcp
        authorization:
          'allow-tool-list':
            patternMatching:
              patterns:
                - predicate: request.headers['x-mcp-method'] in ["tools/list","initialize","notifications/initialized"]
          'authorized-tools':
            opa:
              rego: |
                allow = true
                tools = { server: roles |
                  server := object.keys(input.auth.identity.resource_access)[_];
                  roles := object.get(input.auth.identity.resource_access, server, {}).roles
                }
              allValues: true
        response:
          success:
            headers:
              x-authorized-tools:
                wristband:
                  issuer: 'authorino'
                  customClaims:
                    'allowed-tools':
                      selector: auth.authorization.authorized-tools.tools.@tostr
                  tokenDuration: 300
                  signingKeyRefs:
                    - name: trusted-headers-private-key
                      algorithm: ES256

    How it works:

    • The policy uses OPA (Open Policy Agent) to extract tool permissions from the JWT's resource_access claim.
    • Keycloak stores permissions as client roles, where each MCP server is a resource server client and each tool is a role.
    • The wristband feature creates a signed JWT containing the allowed tools mapping (for example, {"server1.mcp.local":["greet","time"],"server2.mcp.local":["headers"]}).
    • The broker validates this JWT and filters tools before returning the response.

    On the broker side, the implementation is straightforward (internal/broker/filtered_tools_handler.go):

    func (broker *mcpBrokerImpl) FilteredTools(ctx context.Context, _ any,
        mcpReq *mcp.ListToolsRequest, mcpRes *mcp.ListToolsResult) {
        // Get the x-authorized-tools header
        allowedToolsValue := mcpReq.Header[authorizedToolsHeader][0]
        // Validate the JWT signature
        parsedToken, err := validateJWTHeader(allowedToolsValue, broker.trustedHeadersPublicKey)
        // Extract the allowed-tools claim
        authorizedTools := map[string][]string{}
        json.Unmarshal([]byte(allowedToolsValue), &authorizedTools)
        // Filter tools based on permissions
        mcpRes.Tools = broker.filterTools(authorizedTools)
    }

    The benefits include:

    • Least privilege: Users only see tools they're authorized to use.
    • Cryptographically secure: The broker verifies the header signature, preventing tampering.
    • Transparent to clients: No changes needed to MCP client implementations.
    • Flexible permissions model: Supports any identity provider that can issue JWT claims.

    2. OAuth2 Token Exchange

    The problem: AI agents typically authenticate with a single OAuth2 access token that has broad scopes and multiple audiences. Passing this token to every backend MCP server creates a privilege escalation risk—a malicious server could use the token to access other services the user has access to.

    The solution: We implement RFC 8693 OAuth2 Token Exchange to convert the broad access token into narrowly-scoped tokens specific to each MCP server:

    apiVersion: kuadrant.io/v1
    kind: AuthPolicy
    metadata:
      name: mcps-auth-policy
      namespace: gateway-system
    spec:
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: mcp-gateway
        sectionName: mcps
      rules:
        authentication:
          'keycloak':
            jwt:
              issuerUrl: https://keycloak.example.com/realms/mcp
        metadata:
          oauth-token-exchange:
            http:
              url: https://keycloak.example.com/realms/mcp/protocol/openid-connect/token
              method: POST
              credentials:
                authorizationHeader:
                  prefix: Basic
              sharedSecretRef:
                name: token-exchange
                key: oauth-client-basic-auth
              bodyParameters:
                grant_type:
                  value: urn:ietf:params:oauth:grant-type:token-exchange
                subject_token:
                  expression: request.headers['authorization'].split('Bearer ')[1]
                subject_token_type:
                  value: urn:ietf:params:oauth:token-type:access_token
                audience:
                  expression: request.host  # Target MCP server hostname
                scope:
                  value: openid
        authorization:
          'token':
            opa:
              rego: |
                scoped_jwt := object.get(object.get(object.get(input.auth, "metadata", {}),
                    "oauth-token-exchange", {}), "access_token", "")
                jwt := j { scoped_jwt != ""; j := scoped_jwt }
                jwt := j { scoped_jwt == ""; j := split(input.request.headers["authorization"], "Bearer ")[1] }
                claims := c { [_, c, _] := io.jwt.decode(jwt) }
                allow = true
              allValues: true
          'scoped-audience-check':
            patternMatching:
              patterns:
                - predicate: has(auth.authorization.token.claims.aud) &&
                    type(auth.authorization.token.claims.aud) == string &&
                    auth.authorization.token.claims.aud == request.host
          'tool-access-check':
            patternMatching:
              patterns:
                - predicate: |
                    request.headers['x-mcp-toolname'] in (has(auth.authorization.token.claims.resource_access) &&
                      auth.authorization.token.claims.resource_access.exists(p, p == request.host) ?
                      auth.authorization.token.claims.resource_access[request.host].roles : [])
        response:
          success:
            headers:
              authorization:
                plain:
                  expression: "Bearer " + auth.authorization.token.jwt

    How it works:

    1. The MCP Router sets the x-mcp-toolname header based on the tool being called.
    2. The AuthPolicy calls the token exchange endpoint, passing the original token and the target MCP server hostname as the audience.
    3. The identity provider issues a new token with:
      • aud claim set to the target MCP server only.
      • Scopes limited to what's needed for that server.
      • Same user identity claims.
    4. Authorization checks verify:
      • The exchanged token has the correct audience.
      • The user has permission to access the requested tool.
    5. The exchanged token replaces the original in the Authorization header.

    The benefits include:

    • Least privilege tokens: Each backend server receives only the access it needs.
    • Prevents lateral movement: A compromised server can't use the token to access other services.
    • Standards-based: Uses RFC 8693, supported by major identity providers.
    • Transparent: No changes to MCP server implementations.

    3. HashiCorp Vault integration

    The problem: Not all MCP servers support OAuth2. Many external services (like GitHub's MCP server) require PATs or API keys. Managing these credentials securely while still using OAuth2 for user authentication creates an integration challenge.

    The solution: We use the AuthPolicy metadata feature to fetch credentials from HashiCorp Vault, indexed by user identity and target server:

    metadata:
      vault:
        http:
          urlExpression: |
            "http://vault.vault.svc.cluster.local:8200/v1/secret/data/" +
            auth.identity.preferred_username + "/" + request.host
          method: GET
          credentials:
            customHeader:
              name: X-Vault-Token
          sharedSecretRef:
            name: token-exchange
            key: vault-token
        priority: 0  # Try Vault first
      oauth-token-exchange:
        when:
          - predicate: "!has(auth.metadata.vault.data) ||
              !has(auth.metadata.vault.data.data) ||
              !has(auth.metadata.vault.data.data.token) ||
              type(auth.metadata.vault.data.data.token) != string"
        # ... token exchange config ...
        priority: 1  # Fallback to token exchange if Vault has no entry
    The response injection logic checks for Vault credentials first:
    response:
      success:
        headers:
          authorization:
            plain:
              expression: |
                "Bearer " + ((has(auth.metadata.vault.data) &&
                  has(auth.metadata.vault.data.data) &&
                  has(auth.metadata.vault.data.data.token) &&
                  type(auth.metadata.vault.data.data.token) == string) ?
                  auth.metadata.vault.data.data.token :
                  auth.authorization.token.jwt)

    How it works:

    1. The AuthPolicy first tries to fetch a credential from Vault using a path like /v1/secret/data/alice/github.mcp.local.
    2. If found, it uses that credential (PAT or API key) in the Authorization header.
    3. If not found, it falls back to OAuth2 token exchange.
    4. This allows integration with external services that don't support OAuth2.

    The benefits include:

    • Centralized secret management: Credentials stored securely in Vault, not in code or config.
    • Per-user, per-service credentials: Each user can have their own PATs for external services.
    • Fallback strategy: Gracefully handles both OAuth2 and legacy authentication.
    • Audit trail: Vault provides logging of all credential access.

    Implementation architecture

    The complete flow combines all three capabilities:

                      ┌───────────────┐
                      │ MCP Client    │
                      │ (Agent)       │
                      └──────┬────────┘
                             │ OAuth2 Token (broad scopes)
                             ▼
              ┌───────────────────────────────────────────────┐
              │ Gateway (Envoy + Kuadrant AuthPolicies)       │
              │                                               │
              │ 1. Validate JWT                               │
              │ 2. Extract tool permissions                   │
              │ 3. Create x-authorized-tools wristband        │
              │ 4. Check Vault for credentials                │
              │ 5. Or exchange token (RFC 8693)               │
              │ 6. Verify tool access authorization           │
              └────────┬──────────────┬───────────────────────┘
                       │              │
                       ▼              ▼
               ┌────────────┐     ┌─────────────┐
               │ MCP Broker │     │ MCP Router  │
               │            │     │ (ext_proc)  │
               │ - Validates│     │             │
               │   x-author │     │ - Sets      │
               │  ized-tools│     │   x-mcp-    │
               │ - Filters  │     │   toolname  │
               │   tool list│     │ - Routes to │
               │            │     │   backend   │
               └────────────┘     └──────┬──────┘
                                         │ Scoped token or PAT
                                         ▼
                                   ┌─────────────┐
                                   │ Backend MCP │
                                   │ Servers     │
                                   └─────────────┘

    Key design decisions

    • No hard dependencies: The MCP Gateway components (broker, router, controller) don't depend on Kuadrant. They expose extension points (headers, metadata) that any policy engine can use.
    • Defense in depth: Multiple layers of security:
      • Gateway-level authentication (AuthPolicy)
      • Tool-level authorization (x-mcp-toolname checks)
      • Cryptographic verification (wristband signatures)
      • Token scoping (audience and scope reduction)
    • Envoy-first design: All security logic runs in Envoy filters, ensuring consistent policy enforcement regardless of backend implementation.
    • Gateway API integration: Uses standard Gateway API resources (HTTPRoute, Gateway listeners) for routing, making it compatible with any Gateway API provider.

    Real-world example

    Here's how these capabilities work together in a real scenario. 

    Alice, a developer, wants to use an AI agent to access tools from three MCP servers:

    • Internal code review tools (OAuth2)
    • GitHub repository tools (requires GitHub PAT)
    • Weather forecast tools (API key)

    Flow:

    1. Alice authenticates with her identity provider (Keycloak), receiving an OAuth2 access token with audiences for all three servers
    2. The agent calls tools/list:
      • AuthPolicy validates the token
      • OPA extracts Alice's permissions: {"codereview.local":["analyze_pr","suggest_fix"], "github.mcp.local":["list_repos"], "weather.local":["get_forecast"]}
      • Wristband creates signed JWT with these permissions
      • Broker returns only the 4 tools Alice can access
    3. The agent calls codereview_analyze_pr:
      • Router sets x-mcp-toolname: analyze_pr and :authority: codereview.local
      • AuthPolicy exchanges Alice's token for one with aud=codereview.local
      • Verifies Alice has the analyze_pr role for that server
      • Routes with scoped token
    4. The agent calls github_list_repos:
      • Router sets x-mcp-toolname: list_repos and :authority: github.mcp.local
      • AuthPolicy checks Vault at /v1/secret/data/alice/github.mcp.local
      • Finds Alice's GitHub PAT
      • Routes with Authorization: Bearer ghp_...
    5. The agent calls weather_get_forecast:
      • Similar flow, but retrieves API key from Vault

    Throughout this flow:

    • Alice never sees the GitHub PAT or weather API key.
    • Each backend server receives only the credentials it needs.
    • No server receives Alice's original OAuth2 token.
    • Tool access is verified at every step.

    Try it yourself

    The MCP Gateway repository includes a complete working example:

    # Set up a local Kind cluster with everything
    git clone git@github.com:kagenti/mcp-gateway.git && cd mcp-gateway
    make local-env-setup
    # Configure OAuth with token exchange and Vault
    make oauth-token-exchange-example-setup
    # Open the MCP Inspector to test
    make inspect-gateway

    This sets up:

    • Keycloak with example users, groups, and tool permissions
    • MCP Gateway with all three auth capabilities enabled
    • HashiCorp Vault with example credentials
    • Test MCP servers demonstrating different auth methods

    See the documentation for detailed configuration guides.

    Conclusion

    Securing aggregated MCP servers requires more than basic authentication. You need fine-grained authorization, token scoping, and support for heterogeneous authentication mechanisms. By leveraging Kuadrant's AuthPolicy as an add-on, the MCP Gateway can provide enterprise-grade security while maintaining flexibility and avoiding hard dependencies.

    These capabilities demonstrate how modern API gateway patterns can be adapted to the unique challenges of AI agent security, providing the controls needed to safely expose powerful tools to autonomous systems.

    Next steps:

    • Explore the MCP Gateway documentation
    • Read about AuthPolicy
    • Try the OAuth example setup
    • Join the discussion on GitHub

    Related Posts

    • 3 MCP servers you should be using (safely)

    • Kuadrant joins CNCF as a sandbox project

    • Integrate incident detection with OpenShift Lightspeed via MCP

    • Kubernetes MCP server: AI-powered cluster management

    • How I built an agentic application for Docling with MCP

    • How to build a simple agentic AI server with MCP

    Recent Posts

    • LLM Compressor 0.9.0: Attention quantization, MXFP4 support, and more

    • A deep dive into OpenShift Container Platform 4.20 performance

    • Introducing the Dynamic Plug-ins Factory for Developer Hub

    • Diagnose Java applications using Cryostat 4.1

    • AI-driven vulnerability management with Red Hat Lightspeed MCP

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue