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
24 changes: 22 additions & 2 deletions cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,32 @@ type VirtualMCPServerSpec struct {
// PassthroughHeaders is an allowlist of incoming client request header names
// forwarded verbatim to all backends (e.g. an API key the backend resolves to
// a user). Takes precedence over config.PassthroughHeaders. Names must not be
// restricted headers (Host, hop-by-hop, X-Forwarded-*). Forwarded headers are
// attacker-influenceable unless a trusted upstream sets them.
// restricted headers (Host, hop-by-hop, X-Forwarded-*). Authorization and
// Cookie additionally require allowCredentialHeaderPassthrough. Forwarded
// headers are attacker-influenceable unless a trusted upstream sets them.
// +optional
// +listType=atomic
PassthroughHeaders []string `json:"passthroughHeaders,omitempty"`

// AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
// in passthroughHeaders. Defaults to false, which rejects them at startup.
// Host, hop-by-hop, Transfer-Encoding, Content-Length and X-Forwarded-* stay
// rejected regardless. Enabling it here also enables
// config.allowCredentialHeaderPassthrough, and never disables it.
//
// Only enable it when a trusted upstream mints per-backend, audience-scoped
// credentials: Virtual MCP cannot check that the caller's token was ever
// intended for the backends it reaches.
//
// SECURITY: this is safe only because of the backend transport chain's nesting
// order — header-forward is outermost and skips headers already present, auth
// is innermost and sets unconditionally, so backends on a real auth strategy
// get their own token, not the caller's. Reordering those stages, or making
// header-forward overwrite instead of skip, leaks the caller's credential to
// every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
// +optional
AllowCredentialHeaderPassthrough bool `json:"allowCredentialHeaderPassthrough,omitempty"`

// ServiceType specifies the Kubernetes service type for the Virtual MCP server
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
// +kubebuilder:default=ClusterIP
Expand Down
9 changes: 9 additions & 0 deletions cmd/thv-operator/pkg/vmcpconfig/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ func (c *Converter) Convert(
config.PassthroughHeaders = vmcp.Spec.PassthroughHeaders
}

config.AllowCredentialHeaderPassthrough = allowCredentialHeaderPassthrough(vmcp)

// Override name with the CR name (authoritative source)
config.Name = vmcp.Name

Expand Down Expand Up @@ -185,6 +187,13 @@ func (c *Converter) Convert(
return config, authServerRC, nil
}

// allowCredentialHeaderPassthrough resolves the credential-passthrough opt-in.
// The promoted top-level field only ever enables; neither level can switch the
// other off.
func allowCredentialHeaderPassthrough(vmcp *mcpv1beta1.VirtualMCPServer) bool {
return vmcp.Spec.AllowCredentialHeaderPassthrough || vmcp.Spec.Config.AllowCredentialHeaderPassthrough
}

// convertIncomingAuth converts IncomingAuthConfig from CRD to vmcp config.
func (c *Converter) convertIncomingAuth(
ctx context.Context,
Expand Down
37 changes: 37 additions & 0 deletions cmd/thv-operator/pkg/vmcpconfig/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2300,3 +2300,40 @@ func TestConverter_PassthroughHeaders(t *testing.T) {
})
}
}

func TestConverter_AllowCredentialHeaderPassthrough(t *testing.T) {
t.Parallel()

tests := []struct {
name string
topLevel bool // spec.allowCredentialHeaderPassthrough
config bool // spec.config.allowCredentialHeaderPassthrough
want bool
}{
{name: "neither set defaults to false"},
{name: "top-level enables", topLevel: true, want: true},
{name: "auto-passthrough: config-level enables, top-level false does not disable", config: true, want: true},
{name: "both set stays enabled", topLevel: true, config: true, want: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
vmcp := v1beta1test.NewVirtualMCPServer("test-vmcp", "default",
v1beta1test.WithVMCPGroupRef("test-group"),
v1beta1test.WithVMCPIncomingAuth(&mcpv1beta1.IncomingAuthConfig{Type: "anonymous"}),
v1beta1test.WithVMCPConfig(vmcpconfig.Config{AllowCredentialHeaderPassthrough: tt.config}),
v1beta1test.MutateVMCP(func(v *mcpv1beta1.VirtualMCPServer) {
v.Spec.AllowCredentialHeaderPassthrough = tt.topLevel
}),
)

converter := newTestConverter(t, newNoOpMockResolver(t))
ctx := log.IntoContext(context.Background(), logr.Discard())
config, _, err := converter.Convert(ctx, vmcp, nil)
require.NoError(t, err)
require.NotNil(t, config)
assert.Equal(t, tt.want, config.AllowCredentialHeaderPassthrough)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ spec:
spec:
description: VirtualMCPServerSpec defines the desired state of VirtualMCPServer
properties:
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in passthroughHeaders. Defaults to false, which rejects them at startup.
Host, hop-by-hop, Transfer-Encoding, Content-Length and X-Forwarded-* stay
rejected regardless. Enabling it here also enables
config.allowCredentialHeaderPassthrough, and never disables it.

Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: Virtual MCP cannot check that the caller's token was ever
intended for the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
authServerConfig:
description: |-
AuthServerConfig configures an embedded OAuth authorization server.
Expand Down Expand Up @@ -2403,6 +2422,21 @@ spec:
type: object
type: array
type: object
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in PassthroughHeaders. Defaults to false, which rejects them at startup.
Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: vMCP cannot check that the caller's token was ever intended for
the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and Sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
audit:
description: |-
Audit configures audit logging for the Virtual MCP server.
Expand Down Expand Up @@ -4518,8 +4552,9 @@ spec:
PassthroughHeaders is an allowlist of incoming client request header names
forwarded verbatim to all backends (e.g. an API key the backend resolves to
a user). Takes precedence over config.PassthroughHeaders. Names must not be
restricted headers (Host, hop-by-hop, X-Forwarded-*). Forwarded headers are
attacker-influenceable unless a trusted upstream sets them.
restricted headers (Host, hop-by-hop, X-Forwarded-*). Authorization and
Cookie additionally require allowCredentialHeaderPassthrough. Forwarded
headers are attacker-influenceable unless a trusted upstream sets them.
items:
type: string
type: array
Expand Down Expand Up @@ -5032,6 +5067,25 @@ spec:
spec:
description: VirtualMCPServerSpec defines the desired state of VirtualMCPServer
properties:
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in passthroughHeaders. Defaults to false, which rejects them at startup.
Host, hop-by-hop, Transfer-Encoding, Content-Length and X-Forwarded-* stay
rejected regardless. Enabling it here also enables
config.allowCredentialHeaderPassthrough, and never disables it.

Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: Virtual MCP cannot check that the caller's token was ever
intended for the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
authServerConfig:
description: |-
AuthServerConfig configures an embedded OAuth authorization server.
Expand Down Expand Up @@ -7366,6 +7420,21 @@ spec:
type: object
type: array
type: object
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in PassthroughHeaders. Defaults to false, which rejects them at startup.
Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: vMCP cannot check that the caller's token was ever intended for
the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and Sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
audit:
description: |-
Audit configures audit logging for the Virtual MCP server.
Expand Down Expand Up @@ -9481,8 +9550,9 @@ spec:
PassthroughHeaders is an allowlist of incoming client request header names
forwarded verbatim to all backends (e.g. an API key the backend resolves to
a user). Takes precedence over config.PassthroughHeaders. Names must not be
restricted headers (Host, hop-by-hop, X-Forwarded-*). Forwarded headers are
attacker-influenceable unless a trusted upstream sets them.
restricted headers (Host, hop-by-hop, X-Forwarded-*). Authorization and
Cookie additionally require allowCredentialHeaderPassthrough. Forwarded
headers are attacker-influenceable unless a trusted upstream sets them.
items:
type: string
type: array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ spec:
spec:
description: VirtualMCPServerSpec defines the desired state of VirtualMCPServer
properties:
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in passthroughHeaders. Defaults to false, which rejects them at startup.
Host, hop-by-hop, Transfer-Encoding, Content-Length and X-Forwarded-* stay
rejected regardless. Enabling it here also enables
config.allowCredentialHeaderPassthrough, and never disables it.

Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: Virtual MCP cannot check that the caller's token was ever
intended for the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
authServerConfig:
description: |-
AuthServerConfig configures an embedded OAuth authorization server.
Expand Down Expand Up @@ -2406,6 +2425,21 @@ spec:
type: object
type: array
type: object
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in PassthroughHeaders. Defaults to false, which rejects them at startup.
Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: vMCP cannot check that the caller's token was ever intended for
the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and Sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
audit:
description: |-
Audit configures audit logging for the Virtual MCP server.
Expand Down Expand Up @@ -4521,8 +4555,9 @@ spec:
PassthroughHeaders is an allowlist of incoming client request header names
forwarded verbatim to all backends (e.g. an API key the backend resolves to
a user). Takes precedence over config.PassthroughHeaders. Names must not be
restricted headers (Host, hop-by-hop, X-Forwarded-*). Forwarded headers are
attacker-influenceable unless a trusted upstream sets them.
restricted headers (Host, hop-by-hop, X-Forwarded-*). Authorization and
Cookie additionally require allowCredentialHeaderPassthrough. Forwarded
headers are attacker-influenceable unless a trusted upstream sets them.
items:
type: string
type: array
Expand Down Expand Up @@ -5035,6 +5070,25 @@ spec:
spec:
description: VirtualMCPServerSpec defines the desired state of VirtualMCPServer
properties:
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in passthroughHeaders. Defaults to false, which rejects them at startup.
Host, hop-by-hop, Transfer-Encoding, Content-Length and X-Forwarded-* stay
rejected regardless. Enabling it here also enables
config.allowCredentialHeaderPassthrough, and never disables it.

Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: Virtual MCP cannot check that the caller's token was ever
intended for the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
authServerConfig:
description: |-
AuthServerConfig configures an embedded OAuth authorization server.
Expand Down Expand Up @@ -7369,6 +7423,21 @@ spec:
type: object
type: array
type: object
allowCredentialHeaderPassthrough:
description: |-
AllowCredentialHeaderPassthrough opts in to listing Authorization and Cookie
in PassthroughHeaders. Defaults to false, which rejects them at startup.
Only enable it when a trusted upstream mints per-backend, audience-scoped
credentials: vMCP cannot check that the caller's token was ever intended for
the backends it reaches.

SECURITY: this is safe only because of the backend transport chain's nesting
order — header-forward is outermost and skips headers already present, auth
is innermost and Sets unconditionally, so backends on a real auth strategy
get their own token, not the caller's. Reordering those stages, or making
header-forward overwrite instead of skip, leaks the caller's credential to
every backend. See pkg/vmcp/session/internal/backend/mcp_session.go.
type: boolean
audit:
description: |-
Audit configures audit logging for the Virtual MCP server.
Expand Down Expand Up @@ -9484,8 +9553,9 @@ spec:
PassthroughHeaders is an allowlist of incoming client request header names
forwarded verbatim to all backends (e.g. an API key the backend resolves to
a user). Takes precedence over config.PassthroughHeaders. Names must not be
restricted headers (Host, hop-by-hop, X-Forwarded-*). Forwarded headers are
attacker-influenceable unless a trusted upstream sets them.
restricted headers (Host, hop-by-hop, X-Forwarded-*). Authorization and
Cookie additionally require allowCredentialHeaderPassthrough. Forwarded
headers are attacker-influenceable unless a trusted upstream sets them.
items:
type: string
type: array
Expand Down
Loading
Loading