Understand authorization in MCP gateway

Control which authenticated users can access specific Model Context Protocol (MCP) server tools and prompts by setting up authorization in the MCP gateway. The MCP gateway supports authorization approaches including Gateway API policy extensions and other policy engines.

Authorization evaluation process

The following steps represent what happens during an authorization evaluation:

  • Authentication: A user authenticates and receives a JSON Web Token (JWT) with permissions.

  • Tool request: The client makes an MCP tool call, such as tools/call.

  • Request identity check: An AuthPolicy object verifies the JWT and extracts authorization claims.

  • Authorization check: A Common Expression Language (CEL) expression evaluates the requested tool against the user permissions extracted from the JWT.

  • Access decision: The final step is to allow or deny based on the authorization check result.

Important

Ensure that there is not a mismatch between your Envoy proxy layer and upstream application layers in performing URL normalization.

Envoy serves raw request paths to the wasm-shim component unless normalize_path and merge_slashes are explicitly enabled in the Envoy HTTP Connection Manager (HCM) settings. This means that you can treat /admin and //admin as distinct paths in your CEL predicates, and so on.

However, your application receives the requests. That upstream service can strip redundant slashes or decode URL-encoded values. This means it bypasses your configuration, serving the protected route.

Make sure that you configure your upstream service so that you do not unintentionally bypass your own security. For more information, see "Customize your system on path normalization" in the Istio documentation.

Workflow for setting up authorization evaluation

To create an authorization evaluation, you must complete the following steps:

  • Set up authentication

  • Define permissions

  • Specify access-control roles

Red Hat build of Keycloak workflow example

Take the following steps if using Red Hat build of Keycloak:

  • Define your permissions and user claims. Add groups or attributes to the default JWT.

  • Define role-based access by using Red Hat build of Keycloak client roles and group bindings for permission decisions.

  • Give authorization to certain users to control access to individual MCP tools and prompts.

  • Configure your identity provider to include Access Control List (ACL) claims in the tokens it issues.

  • Define complex authorization logic using CEL expressions that evaluate the claims and decide whether to let them through.

Configure MCP gateway authorization with an AuthPolicy

The following example demonstrates using a Connectivity Link AuthPolicy custom resource (CR) with Common Expression Language (CEL) to implement role-based access control. You can use the following general pattern for applying the authorization specific to your use case.

Note

The MCP gateway supports Istio or Gateway API compatible authorization and authenticatio mechanisms. Use the best solution for your OpenShift Container Platform clusters.

Prerequisites
  • You installed MCP gateway.

  • You installed Connectivity Link.

  • You configured a Gateway object with an mcp listener and an mcps listener. The mcps listener is required for internal tools/call routing and authorization.

  • You completed authentication procedures, including creating an AuthPolicy CR on the mcp listener.

  • You configured your identity provider to include group and role claims in JSON Web Tokens (JWT).

  • The identity provider client IDs match the namespaced MCPServerRegistration CR name in the format <namespace>/<mcpserverregistration_name>.

Procedure
  1. Ensure that your identity provider includes the required group and role claims in the issued JWTs. In the following example, Red Hat build of Keycloak is used:

    Example issued OAuth token claims:
    {
      "resource_access": {
        "mcp-ns/arithmetic-mcp-server": {
          "roles": ["add", "sum", "multiply", "divide", "prompt:math_tutor"]
        },
        "mcp-ns/geometry-mcp-server": {
          "roles": ["area", "distance", "volume", "prompt:calculate_area"]
        }
      }
    }
    • The "mcp-ns/arithmetic-mcp-server" specification must match the namespaced name of the MCPServerRegistration CR in the format {namespace}/{name}. For example, if your MCPServerRegistration CR is named arithmetic-mcp-server and is applied in the mcp-ns namespace, the Red Hat build of Keycloak client ID must be mcp-ns/arithmetic-mcp-server.

    • The "roles": ["add", "sum", "multiply", "divide", "prompt:<value>"] parameter and values specify the roles representing the allowed tools and prompts.

  2. Configure authorization by creating an AuthPolicy CR that enforces access control on the mcps listener, as shown in the following example:

    Important

    The authorization AuthPolicy CR must target the mcps listener, not the mcp listener. The mcp listener only handles public traffic and has an authentication-only AuthPolicy CR.

    Example tool and prompt access control AuthPolicy CR
    apiVersion: kuadrant.io/v1
    kind: AuthPolicy
    metadata:
      name: <mcp_tool_auth_policy>
      namespace: <gateway_system>
    spec:
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: <mcp_gateway>
        sectionName: <mcps>
      rules:
        authentication:
          'sso-server':
            jwt:
              issuerUrl: https://<keycloak.example.com>/realms/mcp
        authorization:
          'tool-access-check':
            when:
              - predicate: "request.headers.exists(h, h == 'x-mcp-toolname')"
            patternMatching:
              patterns:
                - predicate: |
                    ('tool:' + request.headers['x-mcp-toolname']) in (has(auth.identity.resource_access) && auth.identity.resource_access.exists(p, p == request.headers['x-mcp-servername']) ? auth.identity.resource_access[request.headers['x-mcp-servername']].roles : [])
          'prompt-access-check':
            when:
              - predicate: "request.headers.exists(h, h == 'x-mcp-promptname')"
            patternMatching:
              patterns:
                - predicate: |
                    ('prompt:' + request.headers['x-mcp-promptname']) in (has(auth.identity.resource_access) && auth.identity.resource_access.exists(p, p == request.headers['x-mcp-servername']) ? auth.identity.resource_access[request.headers['x-mcp-servername']].roles : [])
        response:
          unauthenticated:
            headers:
              'WWW-Authenticate':
                value: Bearer resource_metadata=http://<mcp.example.com:8001>/.well-known/oauth-protected-resource
            body:
              value: |
                {
                  "error": "Unauthorized",
                  "message": "MCP Access denied: Authentication required."
                }
          unauthorized:
            body:
              value: |
                {
                  "error": "Forbidden",
                  "message": "MCP Access denied: Insufficient permissions."
                }
    • Replace metadata.name: with the name of the AuthPolicy.

    • Replace metadata.namespace: with the namespace where the AuthPolicy CR is applied.

    • Replace spec.targetRef.name: with the name of the Gateway CR.

    • The spec.targetRef.sectionName: value must be mcps, which is the internal listener for tool/call authorization. This listener must exist on your Gateway object.

    • Authentication: Validates the JWT token using the configured issuer URL. Replace <keycloak.example.com> with your identity provider hostname.

    • Authorization Logic: CEL expressions check if the user role allows access to the requested tool or prompt. The appropriate check is triggered based on the presence of the x-mcp-toolname or x-mcp-promptname header.

    • CEL Breakdown:

      • request.headers['x-mcp-toolname']: The name of the requested Model Context Protocol (MCP) tool.

      • request.headers['x-mcp-servername']: The namespaced name of the MCP server matching the MCPServerRegistration CR.

      • request.headers['x-mcp-promptname']: The name of the requested MCP prompt.

      • auth.identity.resource_access: The JWT claim containing all roles representing each allowed capability, tool or prompt, that the user can access, grouped by MCP server.

    • Response handling: Custom 401 and 403 responses for unauthenticated and unauthorized access attempts.

      Important

      Ensure that there is not a mismatch between your Envoy proxy layer and upstream application layers in performing URL normalization.

      Envoy serves raw request paths to the wasm-shim component unless normalize_path and merge_slashes are explicitly enabled in the Envoy HTTP Connection Manager (HCM) settings. This means that you can treat /admin and //admin as distinct paths in your CEL predicates, and so on.

      However, your application receives the requests. That upstream service can strip redundant slashes or decode URL-encoded values. This means it bypasses your configuration, serving the protected route.

      Make sure that you configure your upstream service so that you do not unintentionally bypass your own security. For more information, see "Customize your system on path normalization" in the Istio documentation.

  3. Apply the AuthPolicy CR by running the following command:

    $ oc apply -f <mcp_tool_auth_policy.yaml>

    Replace <mcp_tool_auth_policy.yaml> with the name of the AuthPolicy YAML filename.

  4. Optional. Configure authorization by creating an AuthPolicy CR using an Open Policy Agent (OPA) rule, as shown in the following example:

    Example AuthPolicy CR with an OPA rule
    apiVersion: kuadrant.io/v1
    kind: AuthPolicy
    metadata:
      name: <mcp_tool_auth_policy>
      namespace: <gateway_system>
    spec:
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: <mcp_gateway>
        sectionName: <mcps>
      rules:
        authentication:
          'sso-server':
            jwt:
              issuerUrl: https://<keycloak.example.com>/realms/mcp
        authorization:
          'authorized-capabilities':
            opa:
              rego: |
                allow = true
                capabilities = {
                  "tools": { server: tools |
                    server := object.keys(input.auth.identity.resource_access)[]
                    tools := [substring(r, count("tool:"), -1) |
                      r := input.auth.identity.resource_access[server].roles[]
                      startswith(r, "tool:")
                    ]
                  },
                  "prompts": { server: prompts |
                    server := object.keys(input.auth.identity.resource_access)[]
                    prompts := [substring(r, count("prompt:"), -1) |
                      r := input.auth.identity.resource_access[server].roles[]
                      startswith(r, "prompt:")
                    ]
                  }
                }
              allValues: true
    #...
  5. Apply the OPA AuthPolicy CR by running the following command:

    $ oc apply -f <mcp_tool_auth_policy.yaml>

    Replace <mcp_tool_auth_policy.yaml> with the name of the AuthPolicy YAML filename.

Verification
  1. Monitor authorization decisions by checking the AuthPolicy CR status with the following command:

    $ oc get authpolicy <mcp_tool_auth_policy> -n <gateway_system> -o jsonpath='{.status.conditions[?(@.type=="Enforced")].status}'
    Example output
    True
  2. Check the authorization logs by running the following command:

    $ oc logs -n mcp-system -l app.kubernetes.io/name=mcp-gateway