diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md new file mode 100644 index 0000000000..65ce4220c7 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -0,0 +1,97 @@ +--- +title: 防重放攻击 +keywords: [higress,replay-protection] +description: 防重放攻击插件配置参考 +--- + +## 简介 + +Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机数来防止请求重放攻击。每个请求都需要携带一个唯一的 nonce 值,服务器会记录并校验这个值的唯一性,从而防止请求被恶意重放。 + +## 功能说明 + +- **强制或可选的 nonce 校验**:可根据配置决定是否强制要求请求携带 nonce 值。 +- **基于 Redis 的 nonce 唯一性验证**:通过 Redis 存储和校验 nonce 值,确保其唯一性。 +- **可配置的 nonce 有效期**:支持设置 nonce 的有效期,过期后自动失效。 +- **nonce 格式和长度校验**:支持对 nonce 值的格式(Base64)和长度进行验证。 +- **自定义错误响应**:支持配置拒绝请求时的状态码和错误信息。 +- **可自定义 nonce 请求头**:可以自定义携带 nonce 的请求头名称。 + +## 配置说明 + +| 配置项 | 类型 | 必填 | 默认值 | 说明 | +|-------------------|--------|------|-----------------|---------------------------------| +| `force_nonce` | bool | 否 | `true` | 是否强制要求请求携带 nonce 值。 | +| `nonce_header` | string | 否 | `X-Higress-Nonce` | 指定携带 nonce 值的请求头名称。 | +| `nonce_ttl` | int | 否 | `900` | nonce 的有效期(单位:秒)。 | +| `nonce_min_length`| int | 否 | `8` | nonce 值的最小长度。 | +| `nonce_max_length`| int | 否 | `128` | nonce 值的最大长度。 | +| `reject_code` | int | 否 | `429` | 拒绝请求时返回的状态码。 | +| `reject_msg` | string | 否 | `"Duplicate nonce"` | 拒绝请求时返回的错误信息。 | +| `validate_base64` | bool | 否 | `false` | 是否校验 nonce 的 base64 编码格式 | +| `redis.serviceName` | string | 是 | 无 | Redis 服务名称,用于存储 nonce 值。 | +| `redis.servicePort` | int | 否 | `6379` | Redis 服务端口。 | +| `redis.timeout` | int | 否 | `1000` | Redis 操作超时时间(单位:毫秒)。 | +| `redis.keyPrefix` | string | 否 | `"replay-protection"` | Redis 键前缀,用于区分不同的 nonce 键。| + +## 配置示例 + +以下是一个防重放攻击插件的完整配置示例: + +```yaml +force_nonce: true +nonce_header: "X-Higress-Nonce" # 指定 nonce 请求头名称 +nonce_ttl: 900 # nonce 有效期设置为 900 秒 +nonce_min_length: 8 # nonce 最小长度 +nonce_max_length: 128 # nonce 最大长度 +validate_base64: true # 是否开启base64格式校验 +reject_code: 429 # 拒绝请求时返回的状态码 +reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 +redis: + serviceName: "redis.dns" # Redis 服务名称 + servicePort: 6379 # Redis 服务端口 + timeout: 1000 # Redis 操作超时时间 + keyPrefix: "replay-protection" # Redis 键前缀 +``` + +## 使用说明 + +### 请求头要求 + +| 请求头名称 | 是否必须 | 说明 | +|-----------------|----------------|------------------------------------------| +| `X-Higress-Nonce` | 根据 `force_nonce` 配置决定 | 请求中携带的随机生成的 nonce 值,需符合 Base64 格式。 | + +> **注意**:可以通过 `nonce_header` 配置自定义请求头名称,默认值为 `X-Mse-Nonce`。 + +### 使用示例 + +```bash +# Generate nonce +nonce=$(openssl rand -base64 32) + +# Send request +curl -X POST 'https://api.example.com/path' \ + -H "X-Higress-Nonce: $nonce" \ + -d '{"key": "value"}' +``` + +## 返回结果 + +```json +{ + "code": 429, + "message": "Duplicate nonce detected" +} +``` + + +## 错误响应示例 + +| 错误场景 | 状态码 | 错误信息 | +|------------------------|-------|--------------------| +| 缺少 nonce 请求头 | `400` | `Missing nonce header` | +| nonce 长度不符合要求 | `400` | `Invalid nonce length` | +| nonce 格式不符合 Base64 | `400` | `Invalid nonce format` | +| nonce 已被使用(重放攻击) | `429` | `Duplicate nonce` | + diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md new file mode 100644 index 0000000000..25dc7339e0 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -0,0 +1,94 @@ +--- +title: Nonce Replay Protection +keywords: [higress, replay-protection] +description: replay-protection config example +--- + + +## Introduction + +The Nonce (Number used ONCE) replay protection plugin prevents request replay attacks by validating a one-time random number in requests. Each request must carry a unique nonce value, which the server records and validates to prevent malicious request replay. + +## Features + +- Mandatory or optional nonce validation +- Redis-based nonce uniqueness verification +- Configurable nonce TTL +- Custom error responses +- Nonce format and length validation + +## Configuration + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| force_nonce | bool | No | true | Whether to enforce nonce requirement | +| nonce_ttl | int | No | 900 | Nonce validity period (seconds) | +| nonce_header | string | No | X-Higress-Nonce | Request header name for the nonce | +| nonce_ttl | int | No | 900 | Nonce validity period (seconds) | +| nonce_min_length | int | No | 8 | Minimum nonce length | +| nonce_max_length | int | No | 128 | Maximum nonce length | +| reject_code | int | No | 429 | error code when request rejected | +| reject_msg | string | No | "Duplicate nonce" | error massage when request rejected | +| validate_base64 | bool | No | false | Whether to validate the base64 encoding format of the nonce. | +| redis.serviceName | string | Yes | - | Redis service name | +| redis.servicePort | int | No | 6379 | Redis service port | +| redis.timeout | int | No | 1000 | Redis operation timeout (ms) | +| redis.keyPrefix | string | No | "replay-protection" | Redis key prefix | + +## Configuration Example + +```yaml + +force_nonce: true +nonce_ttl: 900 +nonce_header:"" +nonce_min_length: 8 +nonce_max_length: 128 +validate_base64: true +reject_code: 429 +reject_msg: "Duplicate nonce" +redis: + serviceName: "redis.dns" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" +``` + +## Usage + +### Required Headers + +| Header | Required | Description | +|--------|----------|-------------| +| `X-Higress-Nonce` | Depends on force_nonce | Random generated nonce value in base64 format | + +>Note: The default nonce header is X-Higress-Nonce. You can customize it using the nonce_header configuration. + +### Usage Example + +```bash +# Generate nonce +nonce=$(openssl rand -base64 32) + +# Send request +curl -X POST 'https://api.example.com/path' \ + -H "x-Higress-nonce: $nonce" \ + -d '{"key": "value"}' +``` + +## Error Response + +```json +{ + "code": 429, + "message": "Duplicate nonce detected" +} +``` +## Error Response Examples + +| Error Scenario | Status Code | Error Message | +|-----------------------------|-------------|-----------------------------| +| Missing nonce header | `400` | `Missing nonce header` | +| Nonce length not valid | `400` | `Invalid nonce length` | +| Nonce not Base64-encoded | `400` | `Invalid nonce format` | +| Duplicate nonce (replay attack) | `429` | `Duplicate nonce` | diff --git a/plugins/wasm-go/extensions/replay-protection/VERSION b/plugins/wasm-go/extensions/replay-protection/VERSION new file mode 100644 index 0000000000..be0aef5602 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/VERSION @@ -0,0 +1 @@ +1.0.0-alpha \ No newline at end of file diff --git a/plugins/wasm-go/extensions/replay-protection/go.mod b/plugins/wasm-go/extensions/replay-protection/go.mod new file mode 100644 index 0000000000..5cac2ed497 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/go.mod @@ -0,0 +1,20 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/replay-protection + +go 1.19 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.4.2 + github.com/higress-group/proxy-wasm-go-sdk v1.0.0 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/resp v0.1.1 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/plugins/wasm-go/extensions/replay-protection/go.sum b/plugins/wasm-go/extensions/replay-protection/go.sum new file mode 100644 index 0000000000..ac4aef5888 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew= +github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU= +github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= +github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go new file mode 100644 index 0000000000..bbe7760052 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -0,0 +1,175 @@ +package main + +import ( + "fmt" + "regexp" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" + "github.com/tidwall/resp" +) + +func main() { + wrapper.SetCtx( + "replay-protection", + wrapper.ParseConfigBy(parseConfig), + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + ) +} + +type ReplayProtectionConfig struct { + ForceNonce bool // Whether to enforce nonce verification + NonceTTL int // Expiration time of the nonce (in seconds) + Redis RedisConfig + NonceMinLen int // Minimum length of the nonce + NonceMaxLen int // Maximum length of the nonce + NonceHeader string // Name of the nonce heade + ValidateBase64 bool // Whether to validate base64 encoding format + RejectCode uint32 // Response code + RejectMsg string // Response body +} + +type RedisConfig struct { + client wrapper.RedisClient + keyPrefix string +} + +func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper.Log) error { + redisConfig := json.Get("redis") + if !redisConfig.Exists() { + return fmt.Errorf("missing redis config") + } + + config.NonceHeader = json.Get("nonce_header").String() + if config.NonceHeader == "" { + config.NonceHeader = "X-Higress-Nonce" + } + + config.ValidateBase64 = json.Get("validate_base64").Bool() + + config.RejectCode = uint32(json.Get("reject_code").Int()) + if config.RejectCode == 0 { + config.RejectCode = 429 + } + + config.RejectMsg = json.Get("reject_msg").String() + if config.RejectMsg == "" { + config.RejectMsg = "Duplicate nonce" + } + + serviceName := redisConfig.Get("serviceName").String() + if serviceName == "" { + return fmt.Errorf("redis service name is required") + } + + servicePort := redisConfig.Get("servicePort").Int() + if servicePort == 0 { + servicePort = 6379 + } + + username := redisConfig.Get("username").String() + password := redisConfig.Get("password").String() + timeout := redisConfig.Get("timeout").Int() + if timeout == 0 { + timeout = 1000 + } + + keyPrefix := redisConfig.Get("keyPrefix").String() + if keyPrefix == "" { + keyPrefix = "replay-protection" + } + config.Redis.keyPrefix = keyPrefix + + config.ForceNonce = json.Get("force_nonce").Bool() + config.NonceTTL = int(json.Get("nonce_ttl").Int()) + if config.NonceTTL < 1 || config.NonceTTL > 1800 { + config.NonceTTL = 900 + } + + config.Redis.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{ + FQDN: serviceName, + Port: servicePort, + }) + + config.NonceMinLen = int(json.Get("nonce_min_length").Int()) + if config.NonceMinLen == 0 { + config.NonceMinLen = 8 + } + + config.NonceMaxLen = int(json.Get("nonce_max_length").Int()) + if config.NonceMaxLen == 0 { + config.NonceMaxLen = 128 + } + + err := config.Redis.client.Init(username, password, timeout) + if err != nil { + log.Errorf("Failed to initialize Redis client: %v", err) + return fmt.Errorf("Redis initialization error: %w", err) + } + return nil +} + +func validateNonce(nonce string, config *ReplayProtectionConfig) error { + if len(nonce) < config.NonceMinLen || len(nonce) > config.NonceMaxLen { + return fmt.Errorf("invalid nonce length: must be between %d and %d", + config.NonceMinLen, config.NonceMaxLen) + } + if config.ValidateBase64 { + if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { + return fmt.Errorf("invalid nonce format: must be base64 encoded") + } + } + + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { + nonce, _ := proxywasm.GetHttpRequestHeader(config.NonceHeader) + if config.ForceNonce && nonce == "" { + // In force mode, reject the request if the nonce header is missing + log.Warnf("Missing nonce header") + proxywasm.SendHttpResponse(400, nil, []byte("Missing nonce header"), -1) + return types.ActionPause + } + + // If there is no nonce, pass through directly (when not in force mode) + if nonce == "" { + return types.ActionContinue + } + + if err := validateNonce(nonce, &config); err != nil { + log.Warnf("Invalid nonce: %v", err) + proxywasm.SendHttpResponse(400, nil, []byte("Invalid nonce"), -1) + return types.ActionPause + } + + redisKey := fmt.Sprintf("%s:%s", config.Redis.keyPrefix, nonce) + + // Check if the nonce already exists + err := config.Redis.client.SetNX(redisKey, "1", config.NonceTTL, func(response resp.Value) { + if response.Error() != nil { + log.Errorf("Redis error: %v", response.Error()) + proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) + return + } else if len(response.String()) == 0 { + log.Warnf("Duplicate nonce detected: %s", nonce) + proxywasm.SendHttpResponse( + config.RejectCode, + nil, + []byte(fmt.Sprintf("%s: %s", config.RejectMsg, nonce)), + -1, + ) + } else { + proxywasm.ResumeHttpRequest() + } + }) + + if err != nil { + log.Errorf("Redis connection failed: %v", err) + proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) + return types.ActionPause + } + return types.ActionContinue +} diff --git a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go index f4b42e67e7..789ff2696e 100644 --- a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go +++ b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go @@ -44,6 +44,7 @@ type RedisClient interface { Get(key string, callback RedisResponseCallback) error Set(key string, value interface{}, callback RedisResponseCallback) error SetEx(key string, value interface{}, ttl int, callback RedisResponseCallback) error + SetNX(key string, value interface{}, expiration int, callback func(response resp.Value)) error MGet(keys []string, callback RedisResponseCallback) error MSet(kvMap map[string]interface{}, callback RedisResponseCallback) error Incr(key string, callback RedisResponseCallback) error @@ -308,6 +309,22 @@ func (c *RedisClusterClient[C]) SetEx(key string, value interface{}, ttl int, ca return RedisCall(c.cluster, respString(args), callback) } +func (c *RedisClusterClient[C]) SetNX(key string, value interface{}, expiration int, callback func(response resp.Value)) error { + if err := c.checkReadyFunc(); err != nil { + return err + } + args := make([]interface{}, 0) + args = append(args, "set") + args = append(args, key) + args = append(args, value) + args = append(args, "NX") + if expiration > 0 { + args = append(args, "EX") + args = append(args, expiration) + } + return RedisCall(c.cluster, respString(args), callback) +} + func (c *RedisClusterClient[C]) MGet(keys []string, callback RedisResponseCallback) error { if err := c.checkReadyFunc(); err != nil { return err diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go new file mode 100644 index 0000000000..ff9ac77429 --- /dev/null +++ b/test/e2e/conformance/tests/replay-protection.go @@ -0,0 +1,137 @@ +// 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 ( + "crypto/rand" + "encoding/base64" + "testing" + + "github.com/alibaba/higress/test/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" +) + +func init() { + Register(WasmPluginsReplayProtection) +} + +func generateBase64Nonce(length int) string { + bytes := make([]byte, length) + rand.Read(bytes) + return base64.StdEncoding.EncodeToString(bytes) +} + +var WasmPluginsReplayProtection = suite.ConformanceTest{ + ShortName: "WasmPluginsReplayProtection", + Description: "The replay protection wasm plugin prevents replay attacks by validating request nonce.", + Manifests: []string{"tests/replay-protection.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + replayNonce := generateBase64Nonce(32) + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "Missing nonce header", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/", + Method: "GET", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "Invalid nonce not base64 encoded", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/", + Method: "GET", + Headers: map[string]string{ + "X-Higress-Nonce": "invalid-nonce", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "First request with unique nonce returns 200", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/", + Method: "GET", + Headers: map[string]string{ + "X-Higress-Nonce": replayNonce, + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "Second request with repeated nonce returns 429", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/", + Method: "GET", + Headers: map[string]string{ + "X-Higress-Nonce": replayNonce, + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 429, + }, + }, + }, + } + + t.Run("WasmPlugins replay-protection", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml new file mode 100644 index 0000000000..4f5fcf2fc7 --- /dev/null +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -0,0 +1,90 @@ +# Copyright (c) 2022 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. + +# Deploy Redis service +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: higress-conformance-infra +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:6.2 + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: higress-conformance-infra +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: replay-protection + namespace: higress-conformance-infra + annotations: +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +# Configure WasmPlugin +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: replay-protection + namespace: higress-system +spec: + defaultConfig: + force_nonce: true + nonce_ttl: 86400 + nonce_header: "X-Higress-Nonce" + nonce_min_length: 8 + nonce_max_length: 128 + validate_base64: true + reject_code: 429 + reject_msg: "Duplicate nonce" + redis: + serviceName: "redis.higress-conformance-infra.svc.cluster.local" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" + url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm \ No newline at end of file