This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new e7f7f6ae53 feat(data_encryption): self-describing encryption marker 
for plugin values (#14162)
e7f7f6ae53 is described below

commit e7f7f6ae5352fa57a3cae0863ca422c2a074619e
Author: AlinsRan <[email protected]>
AuthorDate: Tue Nov 10 17:03:00 2026 +0800

    feat(data_encryption): self-describing encryption marker for plugin values 
(#14162)
---
 apisix/admin/init.lua                             |   6 +-
 apisix/admin/resource.lua                         |   2 +-
 apisix/admin/utils.lua                            |   4 +-
 apisix/core/data_encryption.lua                   |  47 ++
 apisix/plugin.lua                                 |  89 ++-
 docs/en/latest/plugin-develop.md                  |  16 +-
 docs/zh/latest/plugin-develop.md                  |  16 +-
 t/admin/consumers.t                               |   2 +-
 t/admin/credentials.t                             |  10 +-
 t/admin/data_encrypt_marker.t                     | 380 ++++++++++++
 t/admin/routes4.t                                 |   4 +-
 t/node/chash-hashon.t                             |   4 +-
 t/node/consumer-plugin3.t                         |   2 +-
 t/node/credential-plugin-basic-auth.t             |   2 +-
 t/node/credential-plugin-jwt-auth.t               |   2 +-
 t/node/credential-plugin-key-auth.t               |   2 +-
 t/node/credential-plugin-multi-credentials.t      |   4 +-
 t/node/credential-plugin-set-request-header.t     |   4 +-
 t/node/credential-plugin-work-with-other-plugin.t |   2 +-
 t/node/data_encrypt.t                             |   2 +-
 t/node/data_encrypt2.t                            |  14 +-
 t/node/data_encrypt_marker.t                      | 682 ++++++++++++++++++++++
 t/plugin/authz-casdoor.t                          |   2 +-
 t/plugin/authz-keycloak3.t                        |   2 +-
 t/plugin/csrf.t                                   |   2 +-
 t/plugin/elasticsearch-logger.t                   |   4 +-
 t/plugin/error-log-logger-clickhouse.t            |   2 +-
 t/plugin/google-cloud-logging2.t                  |   2 +-
 t/plugin/jwe-decrypt.t                            |   4 +-
 t/plugin/jwt-auth3.t                              |   2 +-
 t/plugin/kafka-proxy.t                            |   2 +-
 t/plugin/openid-connect2.t                        |   2 +-
 t/plugin/openwhisk.t                              |   2 +-
 t/plugin/rocketmq-logger2.t                       |   2 +-
 t/plugin/sls-logger.t                             |   2 +-
 t/plugin/tencent-cloud-cls.t                      |   2 +-
 36 files changed, 1271 insertions(+), 57 deletions(-)

diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua
index 41b9aced8f..e8f8266520 100644
--- a/apisix/admin/init.lua
+++ b/apisix/admin/init.lua
@@ -257,11 +257,11 @@ local function run()
     if code then
         if code == 200 and method == "get" and plugin.enable_gde() then
             if seg_res == "consumers" or seg_res == "credentials" then
-                utils.decrypt_params(plugin.decrypt_conf, data, 
core.schema.TYPE_CONSUMER)
+                utils.decrypt_params(plugin.decrypt_stored_conf, data, 
core.schema.TYPE_CONSUMER)
             elseif seg_res == "plugin_metadata" then
-                utils.decrypt_params(plugin.decrypt_conf, data, 
core.schema.TYPE_METADATA)
+                utils.decrypt_params(plugin.decrypt_stored_conf, data, 
core.schema.TYPE_METADATA)
             else
-                utils.decrypt_params(plugin.decrypt_conf, data)
+                utils.decrypt_params(plugin.decrypt_stored_conf, data)
             end
         end
 
diff --git a/apisix/admin/resource.lua b/apisix/admin/resource.lua
index 8738976b00..02cd0ea19d 100644
--- a/apisix/admin/resource.lua
+++ b/apisix/admin/resource.lua
@@ -449,7 +449,7 @@ function _M:patch(id, conf, sub_path, args)
     -- them before merging, otherwise the check_conf below will encrypt them
     -- again, resulting in fields that are encrypted multiple times
     if apisix_plugin.enable_gde() then
-        utils.decrypt_params(apisix_plugin.decrypt_conf, res_old.body)
+        utils.decrypt_params(apisix_plugin.decrypt_conf_for_patch, 
res_old.body)
     end
 
     if sub_path and sub_path ~= "" then
diff --git a/apisix/admin/utils.lua b/apisix/admin/utils.lua
index 933d6fa381..ff8fb72bc2 100644
--- a/apisix/admin/utils.lua
+++ b/apisix/admin/utils.lua
@@ -84,7 +84,9 @@ function _M.decrypt_params(decrypt_func, body, schema_type)
     -- list
     if body.list then
         for _, route in ipairs(body.list) do
-            if route.value and route.value.plugins then
+            if route.value and schema_type == core.schema.TYPE_METADATA then
+                decrypt_func(route.value.id, route.value, schema_type)
+            elseif route.value and route.value.plugins then
                 for name, conf in pairs(route.value.plugins) do
                     decrypt_func(name, conf, schema_type)
                 end
diff --git a/apisix/core/data_encryption.lua b/apisix/core/data_encryption.lua
index f2394272b6..c56eb831c4 100644
--- a/apisix/core/data_encryption.lua
+++ b/apisix/core/data_encryption.lua
@@ -29,6 +29,8 @@ local ffi               = require("ffi")
 local C                 = ffi.C
 local ngx_encode_base64 = ngx.encode_base64
 local ngx_decode_base64 = ngx.decode_base64
+local str_byte          = string.byte
+local str_sub           = string.sub
 local type              = type
 local ipairs            = ipairs
 local assert            = assert
@@ -42,6 +44,14 @@ void ERR_clear_error(void);
 local _M = {}
 
 
+-- A value written as MARKER followed by the `encrypt` output is ciphertext
+-- wherever it appears, whatever the `encrypt_fields` of the reader.
+local MARKER = "$ENC$v1$"
+local MARKER_LEN = #MARKER
+local MARKER_FIRST_BYTE = str_byte(MARKER, 1)
+_M.MARKER = MARKER
+
+
 --- Build a table of AES-CBC ciphers from a keyring, each using the key itself
 -- as the IV: a 16-byte key selects AES-128, a 32-byte key selects AES-256.
 -- The CLI schema rejects any other length at startup; a key that still reaches
@@ -158,4 +168,41 @@ function _M.decrypt(value, subject)
 end
 
 
+--- Whether `value` is a string carrying the marker.
+function _M.has_marker(value)
+    return type(value) == "string" and str_byte(value, 1) == MARKER_FIRST_BYTE
+           and str_sub(value, 1, MARKER_LEN) == MARKER
+end
+
+
+--- Encrypt `value` like `_M.encrypt` and prepend the marker. Returns `value`
+-- unchanged when no keyring is configured or the encryption fails.
+function _M.encrypt_with_marker(value)
+    local keyring = get_keyring()
+    if #keyring == 0 then
+        return value
+    end
+
+    local encrypted, err = _M.aes_cbc_encrypt(keyring, value)
+    if not encrypted then
+        log.error("failed to encrypt the data: ", err)
+        return value
+    end
+
+    return MARKER .. encrypted
+end
+
+
+--- Decrypt a value carrying the marker. Unlike `_M.decrypt`, a missing keyring
+-- is an error: a marked value is never usable as it is.
+function _M.decrypt_marked(value)
+    local keyring = get_keyring()
+    if #keyring == 0 then
+        return nil, "no keyring configured"
+    end
+
+    return _M.aes_cbc_decrypt(keyring, str_sub(value, MARKER_LEN + 1))
+end
+
+
 return _M
diff --git a/apisix/plugin.lua b/apisix/plugin.lua
index 632e25c74c..fdcad36a6c 100644
--- a/apisix/plugin.lua
+++ b/apisix/plugin.lua
@@ -1194,7 +1194,17 @@ end
 _M.process_encrypt_field = process_encrypt_field
 
 
-local function decrypt_conf(name, conf, schema_type)
+-- marked values are left to `decrypt_marked_conf`
+local function decrypt_unmarked(value)
+    if core.data_encryption.has_marker(value) then
+        return value
+    end
+
+    return core.data_encryption.decrypt(value)
+end
+
+
+local function decrypt_conf(name, conf, schema_type, decrypt)
     if not enable_gde() then
         return
     end
@@ -1206,13 +1216,67 @@ local function decrypt_conf(name, conf, schema_type)
 
     if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) 
then
         for _, key in ipairs(schema.encrypt_fields) do
-            process_encrypt_field(conf, key, core.data_encryption.decrypt, 
name, "decrypt")
+            process_encrypt_field(conf, key, decrypt or decrypt_unmarked, 
name, "decrypt")
         end
     end
 end
 _M.decrypt_conf = decrypt_conf
 
 
+local function decrypt_field(value)
+    if core.data_encryption.has_marker(value) then
+        return core.data_encryption.decrypt_marked(value)
+    end
+    return core.data_encryption.decrypt(value)
+end
+
+
+-- PATCH only decrypts fields that encrypt_conf will encrypt again on write.
+-- Keep marked values outside encrypt_fields in their original stored form.
+function _M.decrypt_conf_for_patch(name, conf, schema_type)
+    decrypt_conf(name, conf, schema_type, decrypt_field)
+end
+
+
+-- Decrypt in place every string under `conf` that carries the encryption
+-- marker, whatever the `encrypt_fields` of the plugin. A value that fails to
+-- decrypt is logged and kept as it is.
+local function decrypt_marked_conf(name, conf, path)
+    if type(conf) ~= "table" then
+        return
+    end
+
+    for k, v in pairs(conf) do
+        local tv = type(v)
+        if tv == "string" then
+            if core.data_encryption.has_marker(v) then
+                local plain, err = core.data_encryption.decrypt_marked(v)
+                if plain then
+                    conf[k] = plain
+                else
+                    core.log.warn("failed to decrypt the value of plugin [", 
name, "] key [",
+                                  path and path .. "." .. tostring(k) or 
tostring(k), "]: ", err)
+                end
+            end
+
+        elseif tv == "table" then
+            decrypt_marked_conf(name, v, path and path .. "." .. tostring(k) 
or tostring(k))
+        end
+    end
+end
+_M.decrypt_marked_conf = decrypt_marked_conf
+
+
+-- Decrypt a stored plugin conf for the Admin API: the `encrypt_fields`, then
+-- the marked values, when data encryption is enabled.
+function _M.decrypt_stored_conf(name, conf, schema_type)
+    if enable_gde() then
+        decrypt_conf(name, conf, schema_type)
+        decrypt_marked_conf(name, conf)
+    end
+end
+
+
 local function encrypt_conf(name, conf, schema_type)
     if not enable_gde() then
         return
@@ -1225,7 +1289,8 @@ local function encrypt_conf(name, conf, schema_type)
 
     if schema.encrypt_fields and not core.table.isempty(schema.encrypt_fields) 
then
         for _, key in ipairs(schema.encrypt_fields) do
-            process_encrypt_field(conf, key, core.data_encryption.encrypt, 
name, "encrypt")
+            process_encrypt_field(conf, key, 
core.data_encryption.encrypt_with_marker, name,
+                                  "encrypt")
         end
     end
 end
@@ -1240,10 +1305,13 @@ check_plugin_metadata = function(item)
     -- subsystems, while each of them only loads its own plugins.
     local ok, err = check_single_plugin_schema(item.id, item,
                                                core.schema.TYPE_METADATA, 
false, true)
-    -- the schema of an unloaded plugin is unavailable, so decrypting its
-    -- metadata would only produce a "failed to get schema" warning
-    if ok and enable_gde() and local_plugins_hash[item.id] then
-        decrypt_conf(item.id, item, core.schema.TYPE_METADATA)
+    if ok and enable_gde() then
+        -- the schema of an unloaded plugin is unavailable, so decrypting its
+        -- metadata would only produce a "failed to get schema" warning
+        if local_plugins_hash[item.id] then
+            decrypt_conf(item.id, item, core.schema.TYPE_METADATA)
+        end
+        decrypt_marked_conf(item.id, item)
     end
 
     return ok, err
@@ -1312,6 +1380,7 @@ function _M.plugin_checker(item, schema_type)
             -- (e.g. ai-proxy service_account_json JSON parsing) see plaintext
             for name, conf in pairs(item.plugins) do
                 decrypt_conf(name, conf, schema_type)
+                decrypt_marked_conf(name, conf)
             end
         end
 
@@ -1331,6 +1400,12 @@ function _M.stream_plugin_checker(item, in_cp)
         if core.config.type == "yaml" or core.config.type == "json" then
             skip_disabled_plugins = false
         end
+
+        if enable_gde() then
+            for name, conf in pairs(item.plugins) do
+                decrypt_marked_conf(name, conf)
+            end
+        end
         return stream_check_schema(item.plugins, nil, skip_disabled_plugins)
     end
 
diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md
index e384ec18bd..869ad5382a 100644
--- a/docs/en/latest/plugin-develop.md
+++ b/docs/en/latest/plugin-develop.md
@@ -378,7 +378,7 @@ By default, APISIX has `data_encryption` enabled with [two 
default keys](https:/
 ```yaml
 apisix:
     data_encryption:
-        enable: true
+        enable_encrypt_fields: true
         keyring:
             - ...
 ```
@@ -389,6 +389,20 @@ APISIX will try to decrypt the data with keys in the order 
of the keys in the ke
 
 If none of the keys in `keyring` can decrypt the data, the original data is 
used.
 
+### Encryption marker
+
+The Admin API stores every value it encrypts in plugin configurations and 
plugin metadata with the `$ENC$v1$` prefix (the marker) followed by the 
ciphertext described above, so the stored value says by itself that it is 
encrypted.
+
+When `enable_encrypt_fields` is on, APISIX decrypts every string value 
carrying the marker in plugin configurations (routes, services, plugin configs, 
consumers, credentials, consumer groups, global rules and stream routes) and in 
plugin metadata, whether or not the field is declared in `encrypt_fields`. The 
Admin API returns these values decrypted in both single-resource and list GET 
responses, including plugin metadata. When it is off, a marked value is used 
and returned as it is store [...]
+
+PATCH preserves stored marked values outside the current plugin schema's 
`encrypt_fields` unless the request replaces or removes them. Fields in 
`encrypt_fields` are decrypted before merging and encrypted again before 
writing.
+
+:::warning
+
+A version of APISIX that does not recognize the marker uses a marked value as 
it is. Once resources are re-saved by a version that writes the marker, going 
back to a version without marker support requires restoring the data backed up 
before the upgrade, and this affects every encrypted field. Avoid writing 
through the Admin API during a rolling upgrade.
+
+:::
+
 ## Register public API
 
 A plugin can register API which exposes to the public. Take batch-requests 
plugin as an example, this plugin registers `POST /apisix/batch-requests` to 
allow developers to group multiple API requests into a single HTTP 
request/response cycle:
diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md
index 8fa2c0d4e0..bba083d3f2 100644
--- a/docs/zh/latest/plugin-develop.md
+++ b/docs/zh/latest/plugin-develop.md
@@ -337,7 +337,7 @@ encrypt_fields = {"clickhouse.password"}
 ```yaml
 apisix:
     data_encryption:
-        enable: true
+        enable_encrypt_fields: true
         keyring:
             - ...
 ```
@@ -348,6 +348,20 @@ apisix:
 
 如果 `keyring` 中的 key 都无法解密数据,则使用原始数据。
 
+### 加密标记
+
+Admin API 在插件配置和插件元数据中存储加密后的值时,会加上 `$ENC$v1$` 
前缀(即加密标记),后接上文所述的密文,使存储的值本身就能表明它已被加密。
+
+当 `enable_encrypt_fields` 开启时,APISIX 会解密插件配置(路由、服务、插件配置、消费者、凭证、消费者组、全局规则和 
Stream 路由)及插件元数据中所有带标记的字符串值,无论该字段是否在 `encrypt_fields` 中声明。Admin API 的单项和列表 GET 
均返回解密后的值,包括插件元数据。当其关闭时,带标记的值按存储的原值使用和返回。如果带标记的值无法用 `keyring` 解密,会记录错误日志并按原值使用,与 
`encrypt_fields` 中声明的字段解密失败时的处理一致。不带标记的值仍按 `encrypt_fields` 处理。
+
+PATCH 会保留当前插件 schema 的 `encrypt_fields` 之外的原始标记密文,除非请求替换或删除该值。`encrypt_fields` 
中的字段在合并前解密,并在写入前重新加密。
+
+:::warning
+
+不识别加密标记的 APISIX 
版本会把带标记的值当作原值使用。资源被写入标记的版本重新保存后,如需回退到不支持加密标记的版本,必须恢复升级前备份的数据,且这会影响所有加密字段。请避免在滚动升级期间通过
 Admin API 写入配置。
+
+:::
+
 ## 注册公共接口
 
 插件可以注册暴露给公网的接口。以 batch-requests 插件为例,这个插件注册了 `POST /apisix/batch-requests` 
接口,让客户端可以将多个 API 请求组合在一个请求/响应中:
diff --git a/t/admin/consumers.t b/t/admin/consumers.t
index fd958885d3..41a87197c0 100644
--- a/t/admin/consumers.t
+++ b/t/admin/consumers.t
@@ -87,7 +87,7 @@ passed
                         "desc": "new consumer",
                         "plugins": {
                             "key-auth": {
-                                "key": "4y+JvURBE6ZwRbbgaryrhg=="
+                                "key": "$ENC$v1$4y+JvURBE6ZwRbbgaryrhg=="
                             }
                         }
                     },
diff --git a/t/admin/credentials.t b/t/admin/credentials.t
index f3730cb444..1698d91ccc 100644
--- a/t/admin/credentials.t
+++ b/t/admin/credentials.t
@@ -81,7 +81,7 @@ GET /t
                         "plugins": {
                             "basic-auth": {
                                 "username": "the-user",
-                                "password": "WvF5kpaLvIzjuk4GNIMTJg=="
+                                "password": "$ENC$v1$WvF5kpaLvIzjuk4GNIMTJg=="
                             }
                         }
                     }
@@ -119,7 +119,7 @@ passed
                     "value":{
                         "desc":"basic-auth for jack",
                         "id":"credential_a",
-                        
"plugins":{"basic-auth":{"username":"the-new-user","password":"WvF5kpaLvIzjuk4GNIMTJg=="}}
+                        
"plugins":{"basic-auth":{"username":"the-new-user","password":"$ENC$v1$WvF5kpaLvIzjuk4GNIMTJg=="}}
                     },
                     "key":"/apisix/consumers/jack/credentials/credential_a"
                 }]]
@@ -155,7 +155,7 @@ passed
                     "value":{
                         "desc":"key-auth for jack",
                         "id":"credential_b",
-                        
"plugins":{"key-auth":{"key":"JCX7x1qN5e9kHt0GuJfWpw=="}}
+                        
"plugins":{"key-auth":{"key":"$ENC$v1$JCX7x1qN5e9kHt0GuJfWpw=="}}
                     },
                     "key":"/apisix/consumers/jack/credentials/credential_b"
                 }]]
@@ -296,7 +296,7 @@ passed
                          "desc": "new description",
                          "plugins": {
                              "key-auth": {
-                                 "key": "523EisB/dvqlIT9RzfF3ZQ=="
+                                 "key": "$ENC$v1$523EisB/dvqlIT9RzfF3ZQ=="
                              }
                          }
                      }
@@ -449,7 +449,7 @@ passed
                     "value":{
                         "desc":"key-auth for jack",
                         "id":"d79a5aa3",
-                        
"plugins":{"key-auth":{"key":"JCX7x1qN5e9kHt0GuJfWpw=="}}
+                        
"plugins":{"key-auth":{"key":"$ENC$v1$JCX7x1qN5e9kHt0GuJfWpw=="}}
                     },
                     "key":"/apisix/consumers/jack/credentials/d79a5aa3"
                 }]]
diff --git a/t/admin/data_encrypt_marker.t b/t/admin/data_encrypt_marker.t
new file mode 100644
index 0000000000..45223c91e1
--- /dev/null
+++ b/t/admin/data_encrypt_marker.t
@@ -0,0 +1,380 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if (!defined $block->yaml_config) {
+        $block->set_value("yaml_config", <<_EOC_);
+apisix:
+    proxy_mode: http&stream
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+_EOC_
+    }
+
+    if (!$block->error_log && !$block->no_error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+
+    my $extra_init_by_lua = <<_EOC_;
+    local de = require("apisix.core").data_encryption
+    local keyring = de.init_iv_tbl({"edd1c9f0985e76a2"})
+    function _G.mark(v)
+        return "\$ENC\$v1\$" .. assert(de.aes_cbc_encrypt(keyring, v))
+    end
+    function _G.stored(key)
+        local res = assert(require("apisix.core").etcd.get(key))
+        return res.body.node.value
+    end
+    function _G.admin(uri, method, body)
+        local json = require("toolkit.json")
+        local t = require("lib.test_admin").test
+        local code, msg, res = t(uri, method, body and json.encode(body))
+        if code >= 300 then
+            return code, json.decode(msg)
+        end
+        return code, res and json.decode(res)
+    end
+_EOC_
+    $block->set_value("extra_init_by_lua", $extra_init_by_lua);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: encrypt_fields are stored marked, read back as plaintext
+--- config
+    location /t {
+        content_by_lua_block {
+            local code, body = admin("/apisix/admin/consumers", ngx.HTTP_PUT, {
+                username = "foo",
+                plugins = {["basic-auth"] = {username = "foo", password = 
"bar"}},
+            })
+            ngx.say(code)
+
+            ngx.say(stored("/consumers/foo").plugins["basic-auth"].password)
+
+            code, body = admin("/apisix/admin/consumers/foo", ngx.HTTP_GET)
+            ngx.say(body.value.plugins["basic-auth"].password)
+
+            code, body = admin("/apisix/admin/consumers", ngx.HTTP_GET)
+            ngx.say(body.list[1].value.plugins["basic-auth"].password)
+        }
+    }
+--- response_body
+201
+$ENC$v1$77+NmbYqNfN+oLm0aX5akg==
+bar
+bar
+
+
+
+=== TEST 2: the data plane decrypts the marked value
+--- config
+    location /t {
+        content_by_lua_block {
+            local code = admin("/apisix/admin/routes/1", ngx.HTTP_PUT, {
+                uri = "/hello",
+                plugins = {["basic-auth"] = {}},
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say(code)
+        }
+    }
+--- response_body
+201
+
+
+
+=== TEST 3: hit the route
+--- request
+GET /hello
+--- more_headers
+Authorization: Basic Zm9vOmJhcg==
+--- response_body
+hello world
+
+
+
+=== TEST 4: PATCH keeps a marked value encrypted exactly once
+--- config
+    location /t {
+        content_by_lua_block {
+            local code, body = admin("/apisix/admin/routes/1", ngx.HTTP_PUT, {
+                uri = "/hello",
+                plugins = {
+                    ["limit-count"] = {
+                        count = 2,
+                        time_window = 60,
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_password = "secret",
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say(code)
+
+            code = admin("/apisix/admin/routes/1", ngx.HTTP_PATCH, {desc = 
"patched"})
+            ngx.say(code)
+            ngx.say(stored("/routes/1").plugins["limit-count"].redis_password
+                    == mark("secret"))
+
+            code = 
admin("/apisix/admin/routes/1/plugins/limit-count/redis_password",
+                         ngx.HTTP_PATCH, "new-secret")
+            ngx.say(code)
+            ngx.say(stored("/routes/1").plugins["limit-count"].redis_password
+                    == mark("new-secret"))
+
+            code, body = admin("/apisix/admin/routes/1", ngx.HTTP_GET)
+            ngx.say(body.value.desc, " ", 
body.value.plugins["limit-count"].redis_password)
+        }
+    }
+--- response_body
+200
+200
+true
+200
+true
+patched new-secret
+
+
+
+=== TEST 5: plugin metadata is stored marked, read back as plaintext
+--- config
+    location /t {
+        content_by_lua_block {
+            local code, body = 
admin("/apisix/admin/plugin_metadata/azure-functions",
+                                     ngx.HTTP_PUT, {master_apikey = 
"master-key"})
+            ngx.say(code)
+            ngx.say(stored("/plugin_metadata/azure-functions").master_apikey
+                    == mark("master-key"))
+
+            code, body = 
admin("/apisix/admin/plugin_metadata/azure-functions", ngx.HTTP_GET)
+            ngx.say(body.value.master_apikey)
+
+            code, body = admin("/apisix/admin/plugin_metadata", ngx.HTTP_GET)
+            for _, item in ipairs(body.list) do
+                if item.value.id == "azure-functions" then
+                    ngx.say(item.value.master_apikey)
+                end
+            end
+        }
+    }
+--- response_body
+201
+true
+master-key
+master-key
+
+
+
+=== TEST 6: reads return marked values as stored, encryption off
+--- yaml_config
+apisix:
+    proxy_mode: http&stream
+    data_encryption:
+        enable_encrypt_fields: false
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            assert(core.etcd.set("/routes/1", {
+                id = "1",
+                uri = "/hello",
+                plugins = {
+                    ["proxy-rewrite"] = {headers = {set = {["X-Secret"] = 
mark("hdr")}}},
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            }))
+            assert(core.etcd.set("/stream_routes/1", {
+                id = "1",
+                plugins = {
+                    ["limit-conn"] = {
+                        conn = 1,
+                        burst = 0,
+                        default_conn_delay = 0.1,
+                        key = "remote_addr",
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_password = mark("hdr"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            }))
+
+            local code, body = admin("/apisix/admin/routes/1", ngx.HTTP_GET)
+            
ngx.say(body.value.plugins["proxy-rewrite"].headers.set["X-Secret"] == 
mark("hdr"))
+
+            code, body = admin("/apisix/admin/routes", ngx.HTTP_GET)
+            
ngx.say(body.list[1].value.plugins["proxy-rewrite"].headers.set["X-Secret"]
+                    == mark("hdr"))
+
+            code, body = admin("/apisix/admin/stream_routes/1", ngx.HTTP_GET)
+            ngx.say(body.value.plugins["limit-conn"].redis_password == 
mark("hdr"))
+
+            assert(core.etcd.set("/plugin_metadata/azure-functions", {
+                id = "azure-functions",
+                master_apikey = mark("master-key"),
+            }))
+            code, body = 
admin("/apisix/admin/plugin_metadata/azure-functions", ngx.HTTP_GET)
+            ngx.say(body.value.master_apikey == mark("master-key"))
+            code, body = admin("/apisix/admin/plugin_metadata", ngx.HTTP_GET)
+            for _, item in ipairs(body.list) do
+                if item.value.id == "azure-functions" then
+                    ngx.say(item.value.master_apikey == mark("master-key"))
+                end
+            end
+
+            -- PATCH keeps the marked value it did not change encrypted at rest
+            code = admin("/apisix/admin/routes/1", ngx.HTTP_PATCH, {desc = 
"patched"})
+            ngx.say(code)
+            
ngx.say(stored("/routes/1").plugins["proxy-rewrite"].headers.set["X-Secret"]
+                    == mark("hdr"))
+
+            -- a value replaced by the PATCH is user input, stored as given
+            code = 
admin("/apisix/admin/routes/1/plugins/proxy-rewrite/headers/set/X-Secret",
+                         ngx.HTTP_PATCH, "new-hdr")
+            ngx.say(code)
+            
ngx.say(stored("/routes/1").plugins["proxy-rewrite"].headers.set["X-Secret"])
+
+            assert(core.etcd.delete("/stream_routes/1"))
+        }
+    }
+--- response_body
+true
+true
+true
+true
+true
+200
+true
+200
+new-hdr
+
+
+
+=== TEST 7: a stored marked value that fails to decrypt
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            assert(core.etcd.set("/routes/1", {
+                id = "1",
+                uri = "/hello",
+                plugins = {
+                    ["proxy-rewrite"] = {headers = {set = {["X-Secret"] = 
"$ENC$v1$bad"}}},
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            }))
+
+            -- reads return it as stored
+            local code, body = admin("/apisix/admin/routes/1", ngx.HTTP_GET)
+            ngx.say(code, " ", 
body.value.plugins["proxy-rewrite"].headers.set["X-Secret"])
+
+            -- PATCH merges into it and keeps it as stored
+            code, body = admin("/apisix/admin/routes/1", ngx.HTTP_PATCH, {desc 
= "patched"})
+            ngx.say(code, " ", 
stored("/routes/1").plugins["proxy-rewrite"].headers.set["X-Secret"])
+
+            assert(core.etcd.delete("/routes/1"))
+        }
+    }
+--- response_body
+200 $ENC$v1$bad
+200 $ENC$v1$bad
+--- error_log
+failed to decrypt the value of plugin [proxy-rewrite] key 
[headers.set.X-Secret]
+
+
+
+=== TEST 8: PATCH preserves marked fields outside encrypt_fields
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local original = mark("header-secret")
+            assert(core.etcd.set("/routes/1", {
+                id = "1",
+                uri = "/hello",
+                plugins = {
+                    ["proxy-rewrite"] = {headers = {set = {["X-Secret"] = 
original}}},
+                    ["limit-count"] = {
+                        count = 2,
+                        time_window = 60,
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_password = mark("redis-secret"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            }))
+
+            local code = admin("/apisix/admin/routes/1", ngx.HTTP_PATCH, {desc 
= "patched"})
+            ngx.say(code)
+            local conf = stored("/routes/1")
+            ngx.say(conf.desc)
+            ngx.say(conf.plugins["proxy-rewrite"].headers.set["X-Secret"] == 
original)
+            ngx.say(conf.plugins["limit-count"].redis_password == 
mark("redis-secret"))
+
+            code = 
admin("/apisix/admin/routes/1/plugins/limit-count/redis_password",
+                         ngx.HTTP_PATCH, "new-redis-secret")
+            ngx.say(code)
+            conf = stored("/routes/1")
+            ngx.say(conf.plugins["proxy-rewrite"].headers.set["X-Secret"] == 
original)
+            ngx.say(conf.plugins["limit-count"].redis_password == 
mark("new-redis-secret"))
+
+            local body
+            code, body = admin("/apisix/admin/routes/1", ngx.HTTP_GET)
+            
ngx.say(body.value.plugins["proxy-rewrite"].headers.set["X-Secret"])
+            ngx.say(body.value.plugins["limit-count"].redis_password)
+
+            -- An explicit replacement must not restore the previous 
ciphertext.
+            code = 
admin("/apisix/admin/routes/1/plugins/proxy-rewrite/headers/set/X-Secret",
+                         ngx.HTTP_PATCH, "replacement")
+            ngx.say(code)
+            
ngx.say(stored("/routes/1").plugins["proxy-rewrite"].headers.set["X-Secret"])
+        }
+    }
+--- response_body
+200
+patched
+true
+true
+200
+true
+true
+header-secret
+new-redis-secret
+200
+replacement
diff --git a/t/admin/routes4.t b/t/admin/routes4.t
index 7a636b178a..b506ae33cc 100644
--- a/t/admin/routes4.t
+++ b/t/admin/routes4.t
@@ -868,7 +868,7 @@ apisix:
     }
 --- response_body
 userkey
-mt39FazQccyMqt4ctoRV7w==
+$ENC$v1$mt39FazQccyMqt4ctoRV7w==
 
 
 
@@ -944,4 +944,4 @@ apisix:
     }
 --- response_body
 userkey
-mt39FazQccyMqt4ctoRV7w==
+$ENC$v1$mt39FazQccyMqt4ctoRV7w==
diff --git a/t/node/chash-hashon.t b/t/node/chash-hashon.t
index 7329332e3f..7b6cef1001 100644
--- a/t/node/chash-hashon.t
+++ b/t/node/chash-hashon.t
@@ -55,7 +55,7 @@ __DATA__
                         "username": "jack",
                         "plugins": {
                             "key-auth": {
-                                "key": "re62sf0vRJqOBjvJJ6RUcA=="
+                                "key": "$ENC$v1$re62sf0vRJqOBjvJJ6RUcA=="
                             }
                         }
                     }
@@ -83,7 +83,7 @@ __DATA__
                         "username": "tom",
                         "plugins": {
                             "key-auth": {
-                                "key": "RAL/niDfEUpx+ynsoqWDuA=="
+                                "key": "$ENC$v1$RAL/niDfEUpx+ynsoqWDuA=="
                             }
                         }
                     }
diff --git a/t/node/consumer-plugin3.t b/t/node/consumer-plugin3.t
index 345bccd927..2ec4ed4bc7 100644
--- a/t/node/consumer-plugin3.t
+++ b/t/node/consumer-plugin3.t
@@ -83,7 +83,7 @@ apisix:
 GET /t
 --- response_body
 userkey
-mt39FazQccyMqt4ctoRV7w==
+$ENC$v1$mt39FazQccyMqt4ctoRV7w==
 --- no_error_log
 [error]
 
diff --git a/t/node/credential-plugin-basic-auth.t 
b/t/node/credential-plugin-basic-auth.t
index c2e55acd47..88dc88742b 100644
--- a/t/node/credential-plugin-basic-auth.t
+++ b/t/node/credential-plugin-basic-auth.t
@@ -99,7 +99,7 @@ passed
                     "value":{
                         "id":"34010989-ce4e-4d61-9493-b54cca8edb31",
                         "plugins":{
-                            
"basic-auth":{"username":"foo","password":"+kOEVUuRc5rC5ZwvvAMLwg=="}
+                            
"basic-auth":{"username":"foo","password":"$ENC$v1$+kOEVUuRc5rC5ZwvvAMLwg=="}
                         }
                     },
                     
"key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31"
diff --git a/t/node/credential-plugin-jwt-auth.t 
b/t/node/credential-plugin-jwt-auth.t
index f95498d6b1..28087ba2eb 100644
--- a/t/node/credential-plugin-jwt-auth.t
+++ b/t/node/credential-plugin-jwt-auth.t
@@ -99,7 +99,7 @@ passed
                     "value":{
                         "id":"34010989-ce4e-4d61-9493-b54cca8edb31",
                         "plugins":{
-                            "jwt-auth": {"key": "user-key", "secret": 
"kK0lkbzXrE7aiTiyK/Z0Sw=="}
+                            "jwt-auth": {"key": "user-key", "secret": 
"$ENC$v1$kK0lkbzXrE7aiTiyK/Z0Sw=="}
                         }
                     },
                     
"key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31"
diff --git a/t/node/credential-plugin-key-auth.t 
b/t/node/credential-plugin-key-auth.t
index 558616d647..faff48c8da 100644
--- a/t/node/credential-plugin-key-auth.t
+++ b/t/node/credential-plugin-key-auth.t
@@ -99,7 +99,7 @@ passed
                     "value":{
                         "id":"34010989-ce4e-4d61-9493-b54cca8edb31",
                         "plugins":{
-                            "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="}
+                            "key-auth": {"key": 
"$ENC$v1$fsFPtg7BtXMXkvSnS9e1zw=="}
                         }
                     },
                     
"key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31"
diff --git a/t/node/credential-plugin-multi-credentials.t 
b/t/node/credential-plugin-multi-credentials.t
index 6b60bb37b9..a90e878f49 100644
--- a/t/node/credential-plugin-multi-credentials.t
+++ b/t/node/credential-plugin-multi-credentials.t
@@ -101,7 +101,7 @@ passed
                     "value":{
                         "id":"the-first-one",
                         "plugins":{
-                            "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="}
+                            "key-auth": {"key": 
"$ENC$v1$fsFPtg7BtXMXkvSnS9e1zw=="}
                         }
                     },
                     "key":"/apisix/consumers/jack/credentials/the-first-one"
@@ -135,7 +135,7 @@ passed
                     "value":{
                         "id":"the-second-one",
                         "plugins":{
-                            "key-auth": {"key": "QwGua2GjZjOiq+Mj3Mef2g=="}
+                            "key-auth": {"key": 
"$ENC$v1$QwGua2GjZjOiq+Mj3Mef2g=="}
                         }
                     },
                     "key":"/apisix/consumers/jack/credentials/the-second-one"
diff --git a/t/node/credential-plugin-set-request-header.t 
b/t/node/credential-plugin-set-request-header.t
index 51148d0385..61248d5a9e 100644
--- a/t/node/credential-plugin-set-request-header.t
+++ b/t/node/credential-plugin-set-request-header.t
@@ -102,7 +102,7 @@ passed
                     "value":{
                         "id":"34010989-ce4e-4d61-9493-b54cca8edb31",
                         "plugins":{
-                            "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="}
+                            "key-auth": {"key": 
"$ENC$v1$fsFPtg7BtXMXkvSnS9e1zw=="}
                         },
                         "labels": {
                             "custom_id": "271fc4a264bb"
@@ -213,7 +213,7 @@ GET /t
                         "username": "jack",
                         "plugins": {
                             "key-auth": {
-                                "key": "fsFPtg7BtXMXkvSnS9e1zw=="
+                                "key": "$ENC$v1$fsFPtg7BtXMXkvSnS9e1zw=="
                             }
                         }
                     },
diff --git a/t/node/credential-plugin-work-with-other-plugin.t 
b/t/node/credential-plugin-work-with-other-plugin.t
index 14bfc13c91..3e6ede4d1e 100644
--- a/t/node/credential-plugin-work-with-other-plugin.t
+++ b/t/node/credential-plugin-work-with-other-plugin.t
@@ -101,7 +101,7 @@ passed
                     "value":{
                         "id":"34010989-ce4e-4d61-9493-b54cca8edb31",
                         "plugins":{
-                            "key-auth": {"key": "fsFPtg7BtXMXkvSnS9e1zw=="}
+                            "key-auth": {"key": 
"$ENC$v1$fsFPtg7BtXMXkvSnS9e1zw=="}
                         }
                     },
                     
"key":"/apisix/consumers/jack/credentials/34010989-ce4e-4d61-9493-b54cca8edb31"
diff --git a/t/node/data_encrypt.t b/t/node/data_encrypt.t
index c9b9e6d005..d65a0dc7c5 100644
--- a/t/node/data_encrypt.t
+++ b/t/node/data_encrypt.t
@@ -89,7 +89,7 @@ apisix:
     }
 --- response_body
 bar
-77+NmbYqNfN+oLm0aX5akg==
+$ENC$v1$77+NmbYqNfN+oLm0aX5akg==
 
 
 
diff --git a/t/node/data_encrypt2.t b/t/node/data_encrypt2.t
index d9476228da..b0569b825e 100644
--- a/t/node/data_encrypt2.t
+++ b/t/node/data_encrypt2.t
@@ -93,7 +93,7 @@ apisix:
     }
 --- response_body
 abc123
-7ipXoKyiZZUAgf3WWNPI5A==
+$ENC$v1$7ipXoKyiZZUAgf3WWNPI5A==
 
 
 
@@ -181,7 +181,7 @@ apisix:
     }
 --- response_body
 abc123
-7ipXoKyiZZUAgf3WWNPI5A==
+$ENC$v1$7ipXoKyiZZUAgf3WWNPI5A==
 
 
 
@@ -260,7 +260,7 @@ apisix:
     }
 --- response_body
 def456
-3hlZu5mwUbqROm+cy0Vi9A==
+$ENC$v1$3hlZu5mwUbqROm+cy0Vi9A==
 
 
 
@@ -337,7 +337,7 @@ apisix:
     }
 --- response_body
 abc123
-7ipXoKyiZZUAgf3WWNPI5A==
+$ENC$v1$7ipXoKyiZZUAgf3WWNPI5A==
 
 
 
@@ -435,7 +435,7 @@ apisix:
     }
 --- response_body
 abc123
-7ipXoKyiZZUAgf3WWNPI5A==
+$ENC$v1$7ipXoKyiZZUAgf3WWNPI5A==
 
 
 
@@ -558,7 +558,7 @@ apisix:
 GET /t
 --- response_body
 test
-9QKrmTT3TkWGvjlIoe5XXw==
+$ENC$v1$9QKrmTT3TkWGvjlIoe5XXw==
 passed
 
 
@@ -637,7 +637,7 @@ apisix:
     }
 --- response_body
 auth-two
-vU/ZHVJw7b0XscDJ1Fhtig==
+$ENC$v1$vU/ZHVJw7b0XscDJ1Fhtig==
 
 
 
diff --git a/t/node/data_encrypt_marker.t b/t/node/data_encrypt_marker.t
new file mode 100644
index 0000000000..2026db6590
--- /dev/null
+++ b/t/node/data_encrypt_marker.t
@@ -0,0 +1,682 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+log_level("info");
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!defined $block->request && !defined $block->stream_request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if (!defined $block->yaml_config) {
+        $block->set_value("yaml_config", <<_EOC_);
+apisix:
+    data_encryption:
+        enable_encrypt_fields: true
+        keyring:
+            - edd1c9f0985e76a2
+_EOC_
+    }
+
+    if (!$block->error_log && !$block->no_error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+
+    # write the resources straight to etcd, the way the control plane does
+    my $extra_init_by_lua = <<_EOC_;
+    local de = require("apisix.core").data_encryption
+    local keyring = de.init_iv_tbl({"edd1c9f0985e76a2"})
+    function _G.enc(v)
+        return assert(de.aes_cbc_encrypt(keyring, v))
+    end
+    function _G.mark(v)
+        return "\$ENC\$v1\$" .. enc(v)
+    end
+    function _G.get(path, headers)
+        local http = require("resty.http")
+        local res = assert(http.new():request_uri("http://127.0.0.1:"; .. 
ngx.var.server_port
+                                                  .. path, {headers = 
headers}))
+        return res
+    end
+    function _G.del(key)
+        assert(require("apisix.core").etcd.delete(key))
+    end
+    function _G.put(key, value)
+        local res, err = require("apisix.core").etcd.set(key, value)
+        if not res then
+            ngx.say(err)
+            return
+        end
+    end
+    function _G.create_redis_user()
+        -- not every CI job starts Redis with an ACL user, so create one here
+        local red = require("resty.redis"):new()
+        assert(red:connect("127.0.0.1", 6379))
+        assert(red:acl("setuser", "enc-marker", "reset", "on",
+                       ">enc-marker-pass", "~*", "&*", "+\@all"))
+        red:close()
+    end
+_EOC_
+    $block->set_value("extra_init_by_lua", $extra_init_by_lua);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: route: decrypt a marked value of a field outside encrypt_fields
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/routes/1", {
+                uri = "/uri",
+                plugins = {
+                    ["proxy-rewrite"] = {
+                        headers = {
+                            set = {["X-Secret"] = mark("route-secret")},
+                        },
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: hit the route
+--- request
+GET /uri
+--- response_body
+uri: /uri
+host: localhost
+x-real-ip: 127.0.0.1
+x-secret: route-secret
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: marked value that fails to decrypt is logged and kept as it is
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/routes/1", {
+                uri = "/uri",
+                plugins = {
+                    ["proxy-rewrite"] = {
+                        headers = {
+                            set = {["X-Secret"] = "$ENC$v1$" .. 
enc("x"):sub(2)},
+                        },
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.sleep(0.5)
+            -- the route is loaded and the header is set from the value as 
stored
+            ngx.say(get("/uri").body:match("x%-secret: [^\n]*") ~= nil)
+            del("/routes/1")
+        }
+    }
+--- response_body
+true
+--- error_log
+failed to decrypt the value of plugin [proxy-rewrite] key 
[headers.set.X-Secret]
+
+
+
+=== TEST 4: marked values are left as stored when enable_encrypt_fields is off
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: false
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/routes/1", {
+                uri = "/uri",
+                plugins = {
+                    ["response-rewrite"] = {
+                        body = mark("route-secret"),
+                        headers = {
+                            set = {["X-Bad"] = "$ENC$v1$not-a-ciphertext"},
+                        },
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.sleep(0.5)
+            ngx.say(get("/uri").body == mark("route-secret"))
+        }
+    }
+--- response_body
+true
+--- no_error_log
+[error]
+failed to decrypt the value
+
+
+
+=== TEST 5: service and global rule: decrypt marked values
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/services/1", {
+                plugins = {
+                    ["proxy-rewrite"] = {
+                        headers = {
+                            set = {["X-Service-Secret"] = 
mark("service-secret")},
+                        },
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            put("/global_rules/1", {
+                id = "1",
+                plugins = {
+                    ["response-rewrite"] = {
+                        headers = {
+                            set = {["X-Global-Secret"] = 
mark("global-secret")},
+                        },
+                    },
+                },
+            })
+            put("/routes/1", {uri = "/uri", service_id = "1"})
+            ngx.sleep(0.5)
+
+            local res = get("/uri")
+            ngx.say(res.body:match("x%-service%-secret: [^\n]+"))
+            ngx.say(res.headers["X-Global-Secret"])
+            del("/global_rules/1")
+            del("/routes/1")
+            del("/services/1")
+        }
+    }
+--- response_body
+x-service-secret: service-secret
+global-secret
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: consumer: marked key whose plaintext is itself valid ciphertext is 
decrypted once
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/consumers/jack", {
+                username = "jack",
+                plugins = {
+                    ["key-auth"] = {key = mark(enc("jack-key"))},
+                },
+            })
+            put("/routes/1", {
+                uri = "/hello",
+                plugins = {["key-auth"] = {}},
+                upstream = {nodes = {["127.0.0.1:1980"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.sleep(0.5)
+            ngx.say(get("/hello", {apikey = enc("jack-key")}).status)
+            ngx.say(get("/hello", {apikey = "jack-key"}).status)
+        }
+    }
+--- response_body
+200
+401
+
+
+
+=== TEST 7: consumer: unmarked values keep the encrypt_fields behavior
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/consumers/jack", {
+                username = "jack",
+                plugins = {
+                    ["key-auth"] = {key = enc("jack-key")},
+                },
+            })
+            put("/consumers/rose", {
+                username = "rose",
+                plugins = {
+                    ["key-auth"] = {key = "rose-plain-key"},
+                },
+            })
+            ngx.sleep(0.5)
+            for _, key in ipairs({"jack-key", enc("jack-key"), 
"rose-plain-key"}) do
+                ngx.say(get("/hello", {apikey = key}).status)
+            end
+            del("/consumers/rose")
+        }
+    }
+--- response_body
+200
+401
+200
+
+
+
+=== TEST 8: consumer: marked value that fails to decrypt is logged and kept as 
it is
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/consumers/jack", {
+                username = "jack",
+                plugins = {
+                    ["key-auth"] = {key = "$ENC$v1$not-a-ciphertext"},
+                },
+            })
+            ngx.sleep(0.5)
+            ngx.say(get("/hello", {apikey = 
"$ENC$v1$not-a-ciphertext"}).status)
+            ngx.say(get("/hello", {apikey = "jack-key"}).status)
+            del("/consumers/jack")
+            del("/routes/1")
+        }
+    }
+--- response_body
+200
+401
+--- error_log
+failed to decrypt the value of plugin [key-auth] key [key]: base64 decode 
failed
+
+
+
+=== TEST 9: plugin_metadata: decrypt a marked value
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/plugin_metadata/http-logger", {
+                id = "http-logger",
+                log_format = {secret = mark("metadata-secret")},
+            })
+            ngx.sleep(0.5)
+
+            local md = require("apisix.plugin").plugin_metadata("http-logger")
+            ngx.say(md.value.log_format.secret)
+        }
+    }
+--- response_body
+metadata-secret
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: plugin_metadata: marked value that fails to decrypt is logged and 
kept as it is
+--- config
+    location /t {
+        content_by_lua_block {
+            local bad = "$ENC$v1$" .. enc("x"):sub(2)
+            put("/plugin_metadata/http-logger", {
+                id = "http-logger",
+                log_format = {secret = bad},
+            })
+            ngx.sleep(0.5)
+
+            local md = require("apisix.plugin").plugin_metadata("http-logger")
+            ngx.say(md.value.log_format.secret == bad)
+            del("/plugin_metadata/http-logger")
+        }
+    }
+--- response_body
+true
+--- error_log
+failed to decrypt the value of plugin [http-logger] key [log_format.secret]
+
+
+
+=== TEST 11: plugin_metadata: a decrypted marked value is not logged when the 
schema check fails
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/plugin_metadata/http-logger", {
+                id = "http-logger",
+                log_format = {secret = mark("metadata-secret")},
+                log_format_extra = "not-an-object",
+            })
+            ngx.sleep(0.5)
+
+            ngx.say(require("apisix.plugin").plugin_metadata("http-logger"))
+            del("/plugin_metadata/http-logger")
+        }
+    }
+--- response_body
+nil
+--- error_log
+failed to check item data of [/apisix/plugin_metadata]
+--- no_error_log
+metadata-secret
+
+
+
+=== TEST 12: stream route: decrypt a marked value in an array
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/stream_routes/1", {
+                plugins = {
+                    ["ip-restriction"] = {whitelist = {mark("127.0.0.1")}},
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 13: hit the stream route
+--- stream_request
+mmm
+--- stream_response
+hello world
+
+
+
+=== TEST 14: stream route: marked value that fails to decrypt is kept and 
validated as it is
+--- config
+    location /t {
+        content_by_lua_block {
+            del("/stream_routes/1")
+            put("/stream_routes/2", {
+                plugins = {
+                    ["ip-restriction"] = {whitelist = {"$ENC$v1$" .. 
enc("x"):sub(2)}},
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 15: the stream route with the kept value is not loaded
+--- stream_request
+mmm
+--- stream_response
+receive stream response error: connection reset by peer
+--- error_log
+failed to decrypt the value of plugin [ip-restriction] key [whitelist.1]
+
+
+
+=== TEST 16: stream route: limit-conn authenticates to Redis with a marked 
password
+--- config
+    location /t {
+        content_by_lua_block {
+            create_redis_user()
+            del("/stream_routes/2")
+            put("/stream_routes/1", {
+                plugins = {
+                    ["limit-conn"] = {
+                        conn = 1,
+                        burst = 0,
+                        default_conn_delay = 0.1,
+                        key = "remote_addr",
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_port = 6379,
+                        redis_database = 0,
+                        redis_timeout = 1000,
+                        key_ttl = 3600,
+                        redis_username = "enc-marker",
+                        redis_password = mark("enc-marker-pass"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 17: hit the stream route with limit-conn
+--- stream_request
+mmm
+--- stream_response
+hello world
+--- no_error_log
+[error]
+failed to limit conn
+
+
+
+=== TEST 18: stream route: the held connection is counted in Redis and limits 
the next one
+--- stream_upstream_code
+            local sock = ngx.req.socket()
+            sock:receive("1")
+
+            -- runs while the proxied connection holds the only slot
+            local redis = require("resty.redis")
+            local red = redis:new()
+            assert(red:connect("127.0.0.1", 6379))
+            assert(red:auth("enc-marker", "enc-marker-pass"))
+            local keys = 
assert(red:keys("limit_conn:/apisix/stream_routes/1:*"))
+            local held = 0
+            for _, key in ipairs(keys) do
+                held = held + assert(red:zcard(key))
+            end
+
+            local second = ngx.socket.tcp()
+            second:settimeout(2000)
+            assert(second:connect("127.0.0.1", 1985))
+            assert(second:send("mmm"))
+            local data = second:receive("*a")
+            second:close()
+
+            ngx.say("held: ", held, ", second rejected: ", data == nil)
+--- stream_request
+mmm
+--- stream_response
+held: 1, second rejected: true
+--- error_log
+limit-conn exits with status code 503
+
+
+
+=== TEST 19: stream route: limit-conn with a marked password Redis rejects
+--- config
+    location /t {
+        content_by_lua_block {
+            put("/stream_routes/1", {
+                plugins = {
+                    ["limit-conn"] = {
+                        conn = 1,
+                        burst = 0,
+                        default_conn_delay = 0.1,
+                        key = "remote_addr",
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_port = 6379,
+                        redis_database = 0,
+                        redis_timeout = 1000,
+                        key_ttl = 3600,
+                        redis_username = "enc-marker",
+                        redis_password = mark("wrong-pass"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 20: the stream route with the wrong Redis password rejects the 
connection
+--- stream_request
+mmm
+--- stream_response
+receive stream response error: connection reset by peer
+--- error_log
+failed to limit conn: WRONGPASS
+
+
+
+=== TEST 21: stream service: limit-conn authenticates to Redis with a marked 
password
+--- config
+    location /t {
+        content_by_lua_block {
+            create_redis_user()
+            put("/services/1", {
+                plugins = {
+                    ["limit-conn"] = {
+                        conn = 1,
+                        burst = 0,
+                        default_conn_delay = 0.1,
+                        key = "remote_addr",
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_port = 6379,
+                        redis_database = 0,
+                        redis_timeout = 1000,
+                        key_ttl = 3600,
+                        redis_username = "enc-marker",
+                        redis_password = mark("enc-marker-pass"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            put("/stream_routes/1", {service_id = "1"})
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 22: hit the stream route through the stream service
+--- stream_request
+mmm
+--- stream_response
+hello world
+--- no_error_log
+[error]
+failed to limit conn
+
+
+
+=== TEST 23: stream service: the held connection is counted in Redis and 
limits the next one
+--- stream_upstream_code
+            local sock = ngx.req.socket()
+            sock:receive("1")
+
+            -- runs while the proxied connection holds the only slot
+            local redis = require("resty.redis")
+            local red = redis:new()
+            assert(red:connect("127.0.0.1", 6379))
+            assert(red:auth("enc-marker", "enc-marker-pass"))
+            local keys = assert(red:keys("limit_conn:/apisix/services/1:*"))
+            local held = 0
+            for _, key in ipairs(keys) do
+                held = held + assert(red:zcard(key))
+            end
+
+            local second = ngx.socket.tcp()
+            second:settimeout(2000)
+            assert(second:connect("127.0.0.1", 1985))
+            assert(second:send("mmm"))
+            local data = second:receive("*a")
+            second:close()
+
+            ngx.say("held: ", held, ", second rejected: ", data == nil)
+--- stream_request
+mmm
+--- stream_response
+held: 1, second rejected: true
+--- error_log
+limit-conn exits with status code 503
+
+
+
+=== TEST 24: stream route: a marked password is left as stored when the switch 
is off
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: false
+        keyring:
+            - edd1c9f0985e76a2
+--- config
+    location /t {
+        content_by_lua_block {
+            create_redis_user()
+            put("/stream_routes/1", {
+                plugins = {
+                    ["limit-conn"] = {
+                        conn = 1,
+                        burst = 0,
+                        default_conn_delay = 0.1,
+                        key = "remote_addr",
+                        policy = "redis",
+                        redis_host = "127.0.0.1",
+                        redis_port = 6379,
+                        redis_database = 0,
+                        redis_timeout = 1000,
+                        key_ttl = 3600,
+                        redis_username = "enc-marker",
+                        redis_password = mark("enc-marker-pass"),
+                    },
+                },
+                upstream = {nodes = {["127.0.0.1:1995"] = 1}, type = 
"roundrobin"},
+            })
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 25: Redis rejects the password as stored
+--- yaml_config
+apisix:
+    data_encryption:
+        enable_encrypt_fields: false
+        keyring:
+            - edd1c9f0985e76a2
+--- stream_request
+mmm
+--- stream_response
+receive stream response error: connection reset by peer
+--- error_log
+failed to limit conn: WRONGPASS
+--- no_error_log
+failed to decrypt the value
diff --git a/t/plugin/authz-casdoor.t b/t/plugin/authz-casdoor.t
index f73edd748b..3457b5d37d 100644
--- a/t/plugin/authz-casdoor.t
+++ b/t/plugin/authz-casdoor.t
@@ -517,7 +517,7 @@ apisix:
     }
 --- response_body
 3416238e1edf915eac08b8fe345b2b95cdba7e04
-YUfqAO0kPXjZIoAbPSuryCkUDksEmwSq08UDTIUWolN6KQwEUrh72TazePueo4/S
+$ENC$v1$YUfqAO0kPXjZIoAbPSuryCkUDksEmwSq08UDTIUWolN6KQwEUrh72TazePueo4/S
 
 
 
diff --git a/t/plugin/authz-keycloak3.t b/t/plugin/authz-keycloak3.t
index b8a404bb4d..eda09dd428 100644
--- a/t/plugin/authz-keycloak3.t
+++ b/t/plugin/authz-keycloak3.t
@@ -175,7 +175,7 @@ apisix:
     }
 --- response_body
 d1ec69e9-55d2-4109-a3ea-befa071579d5
-Fz1juZEEvh9PPXOmWFdMMJkREt3ZSzEVWcUZPxNP6achk3fosEvn37oN0qH4YgKB
+$ENC$v1$Fz1juZEEvh9PPXOmWFdMMJkREt3ZSzEVWcUZPxNP6achk3fosEvn37oN0qH4YgKB
 
 
 
diff --git a/t/plugin/csrf.t b/t/plugin/csrf.t
index f40c60a80a..b6d65a4414 100644
--- a/t/plugin/csrf.t
+++ b/t/plugin/csrf.t
@@ -387,7 +387,7 @@ apisix:
     }
 --- response_body
 userkey
-mt39FazQccyMqt4ctoRV7w==
+$ENC$v1$mt39FazQccyMqt4ctoRV7w==
 
 
 
diff --git a/t/plugin/elasticsearch-logger.t b/t/plugin/elasticsearch-logger.t
index 34cdf7cb1f..334d9a08bb 100644
--- a/t/plugin/elasticsearch-logger.t
+++ b/t/plugin/elasticsearch-logger.t
@@ -557,7 +557,7 @@ apisix:
     }
 --- response_body
 123456
-PTQvJEaPcNOXcOHeErC0XQ==
+$ENC$v1$PTQvJEaPcNOXcOHeErC0XQ==
 
 
 
@@ -1080,4 +1080,4 @@ apisix:
     }
 --- response_body
 Basic ZWxhc3RpYzoxMjM0NTY=
-8xFrqM3Y8W1eaQ5b6vIPlyOS+HvOyI4agcFeoe47abo=
+$ENC$v1$8xFrqM3Y8W1eaQ5b6vIPlyOS+HvOyI4agcFeoe47abo=
diff --git a/t/plugin/error-log-logger-clickhouse.t 
b/t/plugin/error-log-logger-clickhouse.t
index 91ef60b187..dc7012b7b9 100644
--- a/t/plugin/error-log-logger-clickhouse.t
+++ b/t/plugin/error-log-logger-clickhouse.t
@@ -270,7 +270,7 @@ apisix:
     }
 --- response_body
 bar
-77+NmbYqNfN+oLm0aX5akg==
+$ENC$v1$77+NmbYqNfN+oLm0aX5akg==
 
 
 
diff --git a/t/plugin/google-cloud-logging2.t b/t/plugin/google-cloud-logging2.t
index 35d162b6d5..ad3e449d4b 100644
--- a/t/plugin/google-cloud-logging2.t
+++ b/t/plugin/google-cloud-logging2.t
@@ -193,7 +193,7 @@ 
vjImOadWZbuOVnvEQjb5XCdcOsWsMcg96PtoeuyyHmhnEF1GsMzcIdQv/PHrvYpK
 Yp8D0aqsLEgwGrJQER26FPpKmyIwvcL+nm6q5W31PnU9AOC/WEkB6Zs58hsMzD2S
 kEJQcmfVew5mFXyxuEn3zA==
 -----END PRIVATE KEY-----
-YnwwDKc5vNzo0OU4StTRQbwgCnTZ3dmYiBFm8aGnvTxlE86D2nT07Q3BWhUdky6OGIox4MRLbiHz13NZjyUao/Nudh4PeTj5wMldPD5YvNWtbTG4ig/TNSdBncmIQPLPaUqSweE61pnASxodpTlBJ5k9yxfTmwBTOkzZevoKy9D2E4wF9vGCdkcPK/tAkvRoJTj6xD3xVuAbkcap/81oHplUZZ+ghlEnBZgOH8UMa73UfeNbOQVHD2mlU0LxkTXtwFhHWl50adrt890VDHev0+FUUDjv5Ysl8r/nnnlyq3SV4oqJfs/IVRKROe93e8sJ2/49o7kEv2XT1/6DjM/VsSLKfAi5rLNobcSaSzztSSLkrBFKQvvy2rRA7GFWKbIk+rPZhYmTMItDJv23XP6uzaLRPoq2f/AnRTKpWmA8Dk9TfFHsZLupKi1bmjCdtK8lpMCf9Au1rezt7+2BybQrtbbDbwPzC5bKHmKhc0GPTUzL
 [...]
+$ENC$v1$YnwwDKc5vNzo0OU4StTRQbwgCnTZ3dmYiBFm8aGnvTxlE86D2nT07Q3BWhUdky6OGIox4MRLbiHz13NZjyUao/Nudh4PeTj5wMldPD5YvNWtbTG4ig/TNSdBncmIQPLPaUqSweE61pnASxodpTlBJ5k9yxfTmwBTOkzZevoKy9D2E4wF9vGCdkcPK/tAkvRoJTj6xD3xVuAbkcap/81oHplUZZ+ghlEnBZgOH8UMa73UfeNbOQVHD2mlU0LxkTXtwFhHWl50adrt890VDHev0+FUUDjv5Ysl8r/nnnlyq3SV4oqJfs/IVRKROe93e8sJ2/49o7kEv2XT1/6DjM/VsSLKfAi5rLNobcSaSzztSSLkrBFKQvvy2rRA7GFWKbIk+rPZhYmTMItDJv23XP6uzaLRPoq2f/AnRTKpWmA8Dk9TfFHsZLupKi1bmjCdtK8lpMCf9Au1rezt7+2BybQrtbbDbwPzC5bKHmKh
 [...]
 
 
 
diff --git a/t/plugin/jwe-decrypt.t b/t/plugin/jwe-decrypt.t
index cdf843b217..b7afeb69da 100644
--- a/t/plugin/jwe-decrypt.t
+++ b/t/plugin/jwe-decrypt.t
@@ -226,8 +226,8 @@ passed
         }
     }
 --- response_body
-XU29sA3FEVF68hGcdPo7sg==
-f9pGB0Dt4gYNCLKiINPfVSviKjQs2zfkBCT4+XZ3mDABZkJTr0orzYRD5CptDKMc
+$ENC$v1$XU29sA3FEVF68hGcdPo7sg==
+$ENC$v1$f9pGB0Dt4gYNCLKiINPfVSviKjQs2zfkBCT4+XZ3mDABZkJTr0orzYRD5CptDKMc
 --- no_error_log
 12345678901234567890123456789012
 
diff --git a/t/plugin/jwt-auth3.t b/t/plugin/jwt-auth3.t
index 0c0d33a902..bf94616a77 100755
--- a/t/plugin/jwt-auth3.t
+++ b/t/plugin/jwt-auth3.t
@@ -374,7 +374,7 @@ apisix:
     }
 --- response_body
 my-secret-key
-IRWpPjbDq5BCgHyIllnOMA==
+$ENC$v1$IRWpPjbDq5BCgHyIllnOMA==
 
 
 
diff --git a/t/plugin/kafka-proxy.t b/t/plugin/kafka-proxy.t
index eedc6d986f..a5bdc0f2b3 100644
--- a/t/plugin/kafka-proxy.t
+++ b/t/plugin/kafka-proxy.t
@@ -119,4 +119,4 @@ apisix:
     }
 --- response_body
 admin-secret
-y4Z3aqo51xrt3f9UziNUrg==
+$ENC$v1$y4Z3aqo51xrt3f9UziNUrg==
diff --git a/t/plugin/openid-connect2.t b/t/plugin/openid-connect2.t
index b7877ac332..c15bfb7361 100644
--- a/t/plugin/openid-connect2.t
+++ b/t/plugin/openid-connect2.t
@@ -162,7 +162,7 @@ apisix:
     }
 --- response_body
 60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa
-xMlerg8pE2lPSDlQdPi+MsAwBnzqpyLRar3lUhP2Tdc2oXnWmit92p8cannhDYkBPc6P/Hlx0wSA0T2wle9QyHaW2oqw3bXDQSWWk8Vqq0o=
+$ENC$v1$xMlerg8pE2lPSDlQdPi+MsAwBnzqpyLRar3lUhP2Tdc2oXnWmit92p8cannhDYkBPc6P/Hlx0wSA0T2wle9QyHaW2oqw3bXDQSWWk8Vqq0o=
 
 
 
diff --git a/t/plugin/openwhisk.t b/t/plugin/openwhisk.t
index dfd4d83981..f9cb618a67 100644
--- a/t/plugin/openwhisk.t
+++ b/t/plugin/openwhisk.t
@@ -133,7 +133,7 @@ passed
         }
     }
 --- response_body
-pe14btxogtzJ4qPM/W2qj0AQeUK/O5oegLkKJLkkSEsKUIjP+bgyO+qsTXuLrY/h/esLKrRulD2TOtf+Zt/Us+hxZ/svsMwXZqZ9T9/2wWyi8SKALLfTUZDiV69mxCwD2zNBze1jslMlPtdA9JFIOQ==
+$ENC$v1$pe14btxogtzJ4qPM/W2qj0AQeUK/O5oegLkKJLkkSEsKUIjP+bgyO+qsTXuLrY/h/esLKrRulD2TOtf+Zt/Us+hxZ/svsMwXZqZ9T9/2wWyi8SKALLfTUZDiV69mxCwD2zNBze1jslMlPtdA9JFIOQ==
 
 
 
diff --git a/t/plugin/rocketmq-logger2.t b/t/plugin/rocketmq-logger2.t
index 80c9bf7f8d..f4c064fab8 100644
--- a/t/plugin/rocketmq-logger2.t
+++ b/t/plugin/rocketmq-logger2.t
@@ -691,4 +691,4 @@ apisix:
     }
 --- response_body
 bar
-77+NmbYqNfN+oLm0aX5akg==
+$ENC$v1$77+NmbYqNfN+oLm0aX5akg==
diff --git a/t/plugin/sls-logger.t b/t/plugin/sls-logger.t
index 6bc901e425..83a0798e6f 100644
--- a/t/plugin/sls-logger.t
+++ b/t/plugin/sls-logger.t
@@ -337,7 +337,7 @@ apisix:
     }
 --- response_body
 your_access_key_secret
-1T6nR0fz4yhz/zTuRTvt7Xu3c9ASelDXG2//e/A5OiA=
+$ENC$v1$1T6nR0fz4yhz/zTuRTvt7Xu3c9ASelDXG2//e/A5OiA=
 
 
 
diff --git a/t/plugin/tencent-cloud-cls.t b/t/plugin/tencent-cloud-cls.t
index 9a33fbed61..d1be56b6b0 100644
--- a/t/plugin/tencent-cloud-cls.t
+++ b/t/plugin/tencent-cloud-cls.t
@@ -428,7 +428,7 @@ apisix:
     }
 --- response_body
 secret_key
-oshn8tcqE8cJArmEILVNPQ==
+$ENC$v1$oshn8tcqE8cJArmEILVNPQ==
 
 
 

Reply via email to