Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions api/v1alpha1/mcp_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ type MCPBackendAPIKey struct {
}

// MCPRouteSecurityPolicy defines the security policy for a MCPRoute.
//
// +kubebuilder:validation:XValidation:rule="!has(self.authorization) || has(self.oauth)",message="oauth must be configured when authorization is set"
type MCPRouteSecurityPolicy struct {
// OAuth defines the configuration for the MCP spec compatible OAuth authentication.
//
Expand All @@ -192,6 +194,11 @@ type MCPRouteSecurityPolicy struct {
//
// +optional
ExtAuth *egv1a1.ExtAuth `json:"extAuth,omitempty"`

// Authorization defines the configuration for the MCP spec compatible authorization.
//
// +optional
Authorization *MCPRouteAuthorization `json:"authorization,omitempty"`
}

// MCPRouteOAuth defines a MCP spec compatible OAuth authentication configuration for a MCPRoute.
Expand Down Expand Up @@ -227,6 +234,103 @@ type MCPRouteOAuth struct {
ProtectedResourceMetadata ProtectedResourceMetadata `json:"protectedResourceMetadata"`
}

// MCPRouteAuthorization defines the authorization configuration for a MCPRoute.
type MCPRouteAuthorization struct {
// DefaultAction is the action to take when no rules match. If unspecified, defaults to Deny.
//
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Deny
// +optional
DefaultAction *egv1a1.AuthorizationAction `json:"defaultAction,omitempty"`

// Rules defines a list of authorization rules.
// These rules are evaluated in order, the first matching rule will be applied,
// and the rest will be skipped.
//
// If no rules are defined, the default action will be applied to all requests.
//
// +optional
Rules []MCPRouteAuthorizationRule `json:"rules,omitempty"`
}

// MCPRouteAuthorizationRule defines an authorization rule for MCPRoute based on the MCP authorization spec.
// Reference: https://modelcontextprotocol.io/specification/draft/basic/authorization#scope-challenge-handling
type MCPRouteAuthorizationRule struct {
// Source defines the authorization source for this rule.
// If not specified, the rule will match all sources.
//
// +kubebuilder:validation:Optional
Source *MCPAuthorizationSource `json:"source,omitempty"`

// Target defines the authorization target for this rule.
// If not specified, the rule will match all targets.
//
// +kubebuilder:validation:Optional
Target *MCPAuthorizationTarget `json:"target,omitempty"`

// Action is the authorization decision for matching requests. If unspecified, defaults to Allow.
//
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Allow
// +optional
Action *egv1a1.AuthorizationAction `json:"action,omitempty"`
}

// MCPAuthorizationTarget defines the target of an authorization rule.
type MCPAuthorizationTarget struct {
// Tools defines the list of tools this rule applies to.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for this limitation?

Copy link
Member Author

@zhaohuabing zhaohuabing Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to set a sane limitation for the size of the tool list for one rule. We can adjust it as needed in the future, but I think 16 should be OK for now.

Tools []ToolCall `json:"tools"`
}

// MCPAuthorizationSource defines the source of an authorization rule.
type MCPAuthorizationSource struct {
// JWT defines the JWT scopes required for this rule to match.
//
// +kubebuilder:validation:Required
JWT JWTSource `json:"jwt"`

// TODO: JWTSource can be optional in the future when we support more source types.
}

// JWTSource defines the MCP authorization source for JWT tokens.
type JWTSource struct {
// Scopes defines the list of JWT scopes required for the rule.
// If multiple scopes are specified, all scopes must be present in the JWT for the rule to match.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=16
Scopes []egv1a1.JWTScope `json:"scopes"`

// TODO : we can add more fields in the future, e.g., audiences, claims, etc.
}

// ToolCall represents a tool call in the MCP authorization target.
type ToolCall struct {
// Backend is the name of the backend this tool belongs to.
//
// +kubebuilder:validation:Required
Backend string `json:"backend"`

// Tool is the name of the tool.
//
// +kubebuilder:validation:Required
Tool string `json:"tool"`

// Condition is a CEL expression that must evaluate to true for the rule to match.
// The expression is evaluated with a single variable "args" bound to the tool call arguments as a dynamic object.
// Guard against missing fields with null checks (e.g., args["foo"] != null && args["foo"]["bar"] == "val").
//
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxLength=4096
// +optional
Condition *string `json:"condition,omitempty"`
}

// JWKS defines how to obtain JSON Web Key Sets (JWKS) either from a remote HTTP/HTTPS endpoint or from a local source.
// +kubebuilder:validation:XValidation:rule="has(self.remoteJWKS) || has(self.localJWKS)", message="either remoteJWKS or localJWKS must be specified."
// +kubebuilder:validation:XValidation:rule="!(has(self.remoteJWKS) && has(self.localJWKS))", message="remoteJWKS and localJWKS cannot both be specified."
Expand Down
140 changes: 140 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/envoyproxy/go-control-plane v0.14.0
github.com/envoyproxy/go-control-plane/envoy v1.36.0
github.com/go-logr/logr v1.4.3
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/cel-go v0.26.1
github.com/google/go-cmp v0.7.0
github.com/google/jsonschema-go v0.3.0
Expand Down Expand Up @@ -157,7 +157,6 @@ require (
github.com/go-openapi/validate v0.25.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,6 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
Expand Down
47 changes: 47 additions & 0 deletions internal/controller/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"time"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/go-logr/logr"
"github.com/google/uuid"
appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -471,6 +472,52 @@ func mcpConfig(mcpRoutes []aigv1a1.MCPRoute) *filterapi.MCPConfig {
mcpRoute.Backends = append(
mcpRoute.Backends, mcpBackend)
}
// Add authorization configuration for the route.
if route.Spec.SecurityPolicy != nil && route.Spec.SecurityPolicy.Authorization != nil {
authorization := route.Spec.SecurityPolicy.Authorization
mcpRoute.Authorization = &filterapi.MCPRouteAuthorization{}

if route.Spec.SecurityPolicy.OAuth != nil {
mcpRoute.Authorization.ResourceMetadataURL = buildResourceMetadataURL(&route.Spec.SecurityPolicy.OAuth.ProtectedResourceMetadata)
}

defaultAction := ptr.Deref(authorization.DefaultAction, egv1a1.AuthorizationActionDeny)
mcpRoute.Authorization.DefaultAction = filterapi.AuthorizationAction(defaultAction)

for _, rule := range authorization.Rules {
action := ptr.Deref(rule.Action, egv1a1.AuthorizationActionAllow)
if mcpRoute.Authorization.Rules == nil {
mcpRoute.Authorization.Rules = []filterapi.MCPRouteAuthorizationRule{}
}

tools := make([]filterapi.ToolCall, len(rule.Target.Tools))
for i, tool := range rule.Target.Tools {
tools[i] = filterapi.ToolCall{
Backend: tool.Backend,
Tool: tool.Tool,
Condition: tool.Condition,
}
}

scopes := make([]string, len(rule.Source.JWT.Scopes))
for i, scope := range rule.Source.JWT.Scopes {
scopes[i] = string(scope)
}

mcpRule := filterapi.MCPRouteAuthorizationRule{
Source: &filterapi.MCPAuthorizationSource{
JWT: filterapi.JWTSource{
Scopes: scopes,
},
},
Target: &filterapi.MCPAuthorizationTarget{
Tools: tools,
},
Action: filterapi.AuthorizationAction(action),
}
mcpRoute.Authorization.Rules = append(mcpRoute.Authorization.Rules, mcpRule)
}
}
mc.Routes = append(mc.Routes, mcpRoute)
}
return mc
Expand Down
Loading
Loading