mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
feature: support secret reference for Redis password in MCP Server (#3006)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
@@ -96,6 +96,17 @@ Ingress Config 包含 6 个控制器,各自负责不同的功能:
|
||||
- Http2Rpc Controller:监听 Http2Rpc 资源,实现 HTTP 协议到 RPC 协议的转换。用户可以通过配置协议转换,将 RPC 服务以 HTTP 接口的形式暴露,从而使用 HTTP 请求调用 RPC 接口。
|
||||
- WasmPlugin Controller:监听 WasmPlugin 资源,将 Higress WasmPlugin 转化为 Istio WasmPlugin。Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。
|
||||
- ConfigmapMgr:监听 Higress 的全局配置 `higress-config` ConfigMap,可以根据 tracing、gzip 等配置构造 EnvoyFilter。
|
||||
`mcpServer.redis` 支持通过 Secret 引用保存敏感信息,密码字段可以使用 `passwordSecret` 指向 `higress-system` 命名空间下的 Kubernetes Secret,避免在 ConfigMap 中保存明文密码,例如:
|
||||
|
||||
```yaml
|
||||
higress: |-
|
||||
mcpServer:
|
||||
redis:
|
||||
address: "redis:6379"
|
||||
passwordSecret:
|
||||
name: redis-credentials
|
||||
key: password
|
||||
```
|
||||
|
||||
#### 2.2.2 Cert Server
|
||||
|
||||
@@ -140,4 +151,4 @@ Envoy 核心架构如下图:
|
||||
- [1] [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [2] [Istio 服务注册插件机制代码解析](https://www.zhaohuabing.com/post/2019-02-18-pilot-service-registry-code-analysis/)
|
||||
- [3] [Istio Pilot代码深度解析](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
|
||||
@@ -39,10 +39,22 @@ type RedisConfig struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
// The password for Redis authentication
|
||||
Password string `json:"password,omitempty"`
|
||||
// Reference to a secret containing the password
|
||||
PasswordSecret *SecretKeyReference `json:"passwordSecret,omitempty"`
|
||||
// The database index to use
|
||||
DB int `json:"db,omitempty"`
|
||||
}
|
||||
|
||||
// SecretKeyReference defines a reference to a key within a Kubernetes secret
|
||||
type SecretKeyReference struct {
|
||||
// The namespace of the secret. Defaults to the higress system namespace.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// The name of the secret
|
||||
Name string `json:"name,omitempty"`
|
||||
// The key within the secret data
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig defines the configuration for rate limit
|
||||
type MCPRatelimitConfig struct {
|
||||
// The limit of the rate limit
|
||||
@@ -119,6 +131,15 @@ func validMcpServer(m *McpServer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.Redis != nil && m.Redis.PasswordSecret != nil {
|
||||
if m.Redis.PasswordSecret.Name == "" {
|
||||
return errors.New("redis passwordSecret.name cannot be empty")
|
||||
}
|
||||
if m.Redis.PasswordSecret.Key == "" {
|
||||
return errors.New("redis passwordSecret.key cannot be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if m.EnableUserLevelServer && m.Redis == nil {
|
||||
return errors.New("redis config cannot be empty when user level server is enabled")
|
||||
}
|
||||
@@ -184,6 +205,13 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
Password: mcp.Redis.Password,
|
||||
DB: mcp.Redis.DB,
|
||||
}
|
||||
if mcp.Redis.PasswordSecret != nil {
|
||||
newMcp.Redis.PasswordSecret = &SecretKeyReference{
|
||||
Namespace: mcp.Redis.PasswordSecret.Namespace,
|
||||
Name: mcp.Redis.PasswordSecret.Name,
|
||||
Key: mcp.Redis.PasswordSecret.Key,
|
||||
}
|
||||
}
|
||||
}
|
||||
if mcp.Ratelimit != nil {
|
||||
newMcp.Ratelimit = &MCPRatelimitConfig{
|
||||
@@ -504,12 +532,24 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
// Build redis configuration
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
passwordValue := mcp.Redis.Password
|
||||
if mcp.Redis.PasswordSecret != nil && mcp.Redis.PasswordSecret.Name != "" && mcp.Redis.PasswordSecret.Key != "" {
|
||||
ns := mcp.Redis.PasswordSecret.Namespace
|
||||
if ns == "" {
|
||||
ns = m.Namespace
|
||||
}
|
||||
if ns != "" {
|
||||
passwordValue = fmt.Sprintf("${secret.%s/%s.%s}", ns, mcp.Redis.PasswordSecret.Name, mcp.Redis.PasswordSecret.Key)
|
||||
} else {
|
||||
passwordValue = fmt.Sprintf("${secret.%s.%s}", mcp.Redis.PasswordSecret.Name, mcp.Redis.PasswordSecret.Key)
|
||||
}
|
||||
}
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
"address": "%s",
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"db": %d
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, passwordValue, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// Build rate limit configuration
|
||||
|
||||
@@ -120,6 +120,30 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
{
|
||||
name: "redis config with password secret missing name",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis passwordSecret.name cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "redis config with password secret missing key",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis passwordSecret.key cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "valid config with redis",
|
||||
mcp: &McpServer{
|
||||
@@ -152,6 +176,20 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid config with redis password secret",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -265,7 +303,11 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
@@ -276,7 +318,11 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
@@ -291,7 +337,12 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Namespace: "custom-ns",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
@@ -318,7 +369,12 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Namespace: "custom-ns",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
@@ -706,6 +762,80 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "config with password secret",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Password: "ignored",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "${secret.test-namespace/redis-credentials.password}",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "config with password secret and namespace",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Namespace: "other-ns",
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "${secret.other-ns/redis-credentials.password}",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -57,7 +57,11 @@ data:
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址
|
||||
username: "" # Redis用户名(可选)
|
||||
password: "" # Redis密码(可选)
|
||||
password: "" # Redis密码(可选,明文方式)
|
||||
passwordSecret: # 从 Secret 引用密码(推荐,优先级高于 password)
|
||||
name: redis-credentials # Secret 名称
|
||||
key: password # Secret 中的 key
|
||||
namespace: higress-system # Secret 所在命名空间(可选,默认为 higress-system)
|
||||
db: 0 # Redis数据库(可选)
|
||||
match_list: # MCP Server 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
|
||||
- match_rule_domain: "*"
|
||||
|
||||
@@ -57,7 +57,11 @@ data:
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address
|
||||
username: "" # Redis username (optional)
|
||||
password: "" # Redis password (optional)
|
||||
password: "" # Redis password (optional, plaintext)
|
||||
passwordSecret: # Reference password from Secret (recommended, higher priority than password)
|
||||
name: redis-credentials # Secret name
|
||||
key: password # Key in Secret
|
||||
namespace: higress-system # Secret namespace (optional, defaults to higress-system)
|
||||
db: 0 # Redis database (optional)
|
||||
match_list: # MCP Server session persistence routing rules (when matching the following paths, it will be recognized as an MCP session and maintained through SSE)
|
||||
- match_rule_domain: "*"
|
||||
|
||||
87
test/e2e/conformance/tests/configmap-mcp-redis-secret.go
Normal file
87
test/e2e/conformance/tests/configmap-mcp-redis-secret.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/alibaba/higress/v2/pkg/ingress/kube/configmap"
|
||||
"github.com/alibaba/higress/v2/test/e2e/conformance/utils/envoy"
|
||||
"github.com/alibaba/higress/v2/test/e2e/conformance/utils/kubernetes"
|
||||
"github.com/alibaba/higress/v2/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(ConfigMapMcpRedisSecret)
|
||||
}
|
||||
|
||||
var ConfigMapMcpRedisSecret = suite.ConformanceTest{
|
||||
ShortName: "ConfigMapMcpRedisSecret",
|
||||
Description: "Envoy MCP session filter should resolve Redis password from Kubernetes secret and react to updates",
|
||||
Manifests: []string{"tests/configmap-mcp-redis-secret.yaml"},
|
||||
Features: []suite.SupportedFeature{suite.EnvoyConfigConformanceFeature},
|
||||
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||
const (
|
||||
configMapNamespace = "higress-system"
|
||||
configMapName = "higress-config"
|
||||
configMapKey = "higress"
|
||||
secretNamespace = "higress-system"
|
||||
secretName = "redis-credentials"
|
||||
secretKey = "password"
|
||||
|
||||
initialSecretValue = "InitialSecretFromSecret123"
|
||||
updatedSecretValue = "UpdatedSecretFromSecret456"
|
||||
)
|
||||
|
||||
higressCfg := &configmap.HigressConfig{
|
||||
McpServer: &configmap.McpServer{
|
||||
Enable: true,
|
||||
SSEPathSuffix: "/sse",
|
||||
Redis: &configmap.RedisConfig{
|
||||
Address: "redis:6379",
|
||||
PasswordSecret: &configmap.SecretKeyReference{
|
||||
Name: secretName,
|
||||
Key: secretKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := kubernetes.ApplyConfigmapDataWithYaml(t, suite.Client, configMapNamespace, configMapName, configMapKey, higressCfg)
|
||||
require.NoErrorf(t, err, "failed to update %s/%s", configMapNamespace, configMapName)
|
||||
|
||||
assertRedisPassword := func(password string) {
|
||||
envoy.AssertEnvoyConfig(t, suite.TimeoutConfig, envoy.Assertion{
|
||||
Path: `configs.#(@type=="type.googleapis.com/envoy.admin.v3.EcdsConfigDump").` +
|
||||
`ecds_filters.#(ecds_filter.name=="golang-filter-mcp-session").` +
|
||||
`ecds_filter.typed_config.plugin_config.value.redis`,
|
||||
CheckType: envoy.CheckTypeMatch,
|
||||
TargetNamespace: configMapNamespace,
|
||||
ExpectEnvoyConfig: map[string]interface{}{
|
||||
"password": password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
assertRedisPassword(initialSecretValue)
|
||||
|
||||
err = kubernetes.ApplySecret(t, suite.Client, secretNamespace, secretName, secretKey, updatedSecretValue)
|
||||
require.NoErrorf(t, err, "failed to update %s/%s secret", secretNamespace, secretName)
|
||||
|
||||
assertRedisPassword(updatedSecretValue)
|
||||
},
|
||||
}
|
||||
41
test/e2e/conformance/tests/configmap-mcp-redis-secret.yaml
Normal file
41
test/e2e/conformance/tests/configmap-mcp-redis-secret.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: redis-credentials
|
||||
namespace: higress-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
password: InitialSecretFromSecret123
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: higress-conformance-infra-configmap-mcp-redis-secret-test
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "mcp-redis-secret.example.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/mcp"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v3
|
||||
port:
|
||||
number: 8080
|
||||
Reference in New Issue
Block a user