You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/12/01 01:35:32 UTC

[apisix] branch master updated: feat: Support store secrets in secrets manager for auth plugin via kms components (#8421)

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

spacewander 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 0ed451a32 feat: Support store secrets in secrets manager for auth plugin via kms components (#8421)
0ed451a32 is described below

commit 0ed451a32e9b7ba9ee4750b6a02289ed26c8ae2b
Author: soulbird <zh...@outlook.com>
AuthorDate: Thu Dec 1 09:35:22 2022 +0800

    feat: Support store secrets in secrets manager for auth plugin via kms components (#8421)
    
    Co-authored-by: soulbird <zh...@gmail.com>
---
 apisix/admin/kms.lua              |  11 +-
 apisix/consumer.lua               |   3 +-
 apisix/core/env.lua               |  57 +++---
 apisix/core/utils.lua             |  56 ------
 apisix/init.lua                   |   2 +
 apisix/kms.lua                    | 239 ++++++++++++++++++++++
 apisix/kms/vault.lua              |  23 ++-
 apisix/schema_def.lua             |  15 --
 docs/assets/images/kms.png        | Bin 0 -> 125602 bytes
 docs/en/latest/terminology/kms.md |  74 ++++++-
 docs/zh/latest/terminology/kms.md |  70 ++++++-
 t/config-center-yaml/kms.t        | 413 ++++++++++++++++++++++++++++++++++++++
 t/core/env.t                      |  38 ++--
 t/core/utils.t                    | 111 ----------
 t/plugin/key-auth.t               |  84 +++++++-
 15 files changed, 958 insertions(+), 238 deletions(-)

diff --git a/apisix/admin/kms.lua b/apisix/admin/kms.lua
index 895b60421..4be84a7e8 100644
--- a/apisix/admin/kms.lua
+++ b/apisix/admin/kms.lua
@@ -14,10 +14,14 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
+local require = require
+
 local core = require("apisix.core")
 local utils = require("apisix.admin.utils")
+
 local type = type
 local tostring = tostring
+local pcall = pcall
 
 
 local _M = {
@@ -46,7 +50,12 @@ local function check_conf(id, conf, need_id, typ)
     conf.id = id
 
     core.log.info("conf: ", core.json.delay_encode(conf))
-    local ok, err = core.schema.check(core.schema["kms_" .. typ], conf)
+    local ok, kms_service = pcall(require, "apisix.kms." .. typ)
+    if not ok then
+        return false, {error_msg = "invalid kms service: " .. typ}
+    end
+
+    local ok, err = core.schema.check(kms_service.schema, conf)
     if not ok then
         return nil, {error_msg = "invalid configuration: " .. err}
     end
diff --git a/apisix/consumer.lua b/apisix/consumer.lua
index 32ff69275..bdeb78cd3 100644
--- a/apisix/consumer.lua
+++ b/apisix/consumer.lua
@@ -15,6 +15,7 @@
 -- limitations under the License.
 --
 local core           = require("apisix.core")
+local kms            = require("apisix.kms")
 local plugin         = require("apisix.plugin")
 local plugin_checker = require("apisix.plugin").plugin_checker
 local error          = error
@@ -103,7 +104,7 @@ local function create_consume_cache(consumers_conf, key_attr)
     for _, consumer in ipairs(consumers_conf.nodes) do
         core.log.info("consumer node: ", core.json.delay_encode(consumer))
         local new_consumer = core.table.clone(consumer)
-        new_consumer.auth_conf = core.utils.retrieve_secrets_ref(new_consumer.auth_conf)
+        new_consumer.auth_conf = kms.fetch_secrets(new_consumer.auth_conf)
         consumer_names[new_consumer.auth_conf[key_attr]] = new_consumer
     end
 
diff --git a/apisix/core/env.lua b/apisix/core/env.lua
index d8702904a..2bab04327 100644
--- a/apisix/core/env.lua
+++ b/apisix/core/env.lua
@@ -27,10 +27,13 @@ local find        = string.find
 local sub         = string.sub
 local str         = ffi.string
 
-local _M = {}
-
 local ENV_PREFIX = "$ENV://"
 
+local _M = {
+    PREFIX = ENV_PREFIX
+}
+
+
 local apisix_env_vars = {}
 
 ffi.cdef [[
@@ -39,32 +42,35 @@ ffi.cdef [[
 
 
 function _M.init()
-  local e = ffi.C.environ
-  if not e then
-    log.warn("could not access environment variables")
-    return
-  end
-
-  local i = 0
-  while e[i] ~= nil do
-    local var = str(e[i])
-    local p = find(var, "=")
-    if p then
-        apisix_env_vars[sub(var, 1, p - 1)] = sub(var, p + 1)
+    local e = ffi.C.environ
+    if not e then
+        log.warn("could not access environment variables")
+        return
     end
 
-    i = i + 1
-  end
+    local i = 0
+    while e[i] ~= nil do
+        local var = str(e[i])
+        local p = find(var, "=")
+        if p then
+            apisix_env_vars[sub(var, 1, p - 1)] = sub(var, p + 1)
+        end
+
+        i = i + 1
+    end
 end
 
 
-local function is_env_uri(env_uri)
+local function parse_env_uri(env_uri)
     -- Avoid the error caused by has_prefix to cause a crash.
-    return type(env_uri) == "string" and string.has_prefix(upper(env_uri), ENV_PREFIX)
-end
+    if type(env_uri) ~= "string" then
+        return nil, "error env_uri type: " .. type(env_uri)
+    end
 
+    if not string.has_prefix(upper(env_uri), ENV_PREFIX) then
+        return nil, "error env_uri prefix: " .. env_uri
+    end
 
-local function parse_env_uri(env_uri)
     local path = sub(env_uri, #ENV_PREFIX + 1)
     local idx = find(path, "/")
     if not idx then
@@ -80,18 +86,17 @@ local function parse_env_uri(env_uri)
 end
 
 
-function _M.get(env_uri)
-    if not is_env_uri(env_uri) then
-        return nil
+function _M.fetch_by_uri(env_uri)
+    local opts, err = parse_env_uri(env_uri)
+    if not opts then
+        return nil, err
     end
 
-    local opts = parse_env_uri(env_uri)
     local main_value = apisix_env_vars[opts.key] or os.getenv(opts.key)
     if main_value and opts.sub_key ~= "" then
         local vt, err = json.decode(main_value)
         if not vt then
-          log.warn("decode failed, err: ", err, " value: ", main_value)
-          return nil
+            return nil, "decode failed, err: " .. (err or "") .. ", value: " .. main_value
         end
         return vt[opts.sub_key]
     end
diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua
index 6b61cf020..f72996b78 100644
--- a/apisix/core/utils.lua
+++ b/apisix/core/utils.lua
@@ -25,8 +25,6 @@ local rfind_char     = core_str.rfind_char
 local table          = require("apisix.core.table")
 local log            = require("apisix.core.log")
 local string         = require("apisix.core.string")
-local env            = require("apisix.core.env")
-local lrucache       = require("apisix.core.lrucache")
 local dns_client     = require("apisix.core.dns.client")
 local ngx_re         = require("ngx.re")
 local ipmatcher      = require("resty.ipmatcher")
@@ -37,7 +35,6 @@ local sub_str        = string.sub
 local str_byte       = string.byte
 local tonumber       = tonumber
 local tostring       = tostring
-local pairs          = pairs
 local re_gsub        = ngx.re.gsub
 local type           = type
 local io_popen       = io.popen
@@ -332,57 +329,4 @@ end
 _M.resolve_var = resolve_var
 
 
-local secrets_lrucache = lrucache.new({
-    ttl = 300, count = 512
-})
-
-local retrieve_secrets_ref
-do
-    local retrieve_ref
-    function retrieve_ref(refs)
-        for k, v in pairs(refs) do
-            local typ = type(v)
-            if typ == "string" then
-                refs[k] = env.get(v) or v
-            elseif typ == "table" then
-                retrieve_ref(v)
-            end
-        end
-        return refs
-    end
-
-    local function retrieve(refs)
-        log.info("retrieve secrets refs")
-
-        local new_refs = table.deepcopy(refs)
-        return retrieve_ref(new_refs)
-    end
-
-    function retrieve_secrets_ref(refs, cache, key, version)
-        if not refs or type(refs) ~= "table" then
-            return nil
-        end
-        if not cache then
-            return retrieve(refs)
-        end
-        return secrets_lrucache(key, version, retrieve, refs)
-    end
-end
--- Retrieve all secrets ref in the given table
----
--- Retrieve all secrets ref in the given table,
--- and then replace them with the values from the environment variables.
---
--- @function core.utils.retrieve_secrets_ref
--- @tparam table refs The table to be retrieved.
--- @tparam boolean cache Whether to use lrucache to cache results.
--- @tparam string key The cache key for lrucache.
--- @tparam string version The cache version for lrucache.
--- @treturn table The table after the reference is replaced.
--- @usage
--- local new_refs = core.utils.retrieve_secrets_ref(refs) -- "no cache"
--- local new_refs = core.utils.retrieve_secrets_ref(refs, true, key, ver) -- "cache"
-_M.retrieve_secrets_ref = retrieve_secrets_ref
-
-
 return _M
diff --git a/apisix/init.lua b/apisix/init.lua
index fbb090bee..038eb578b 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -37,6 +37,7 @@ local admin_init      = require("apisix.admin.init")
 local get_var         = require("resty.ngxvar").fetch
 local router          = require("apisix.router")
 local apisix_upstream = require("apisix.upstream")
+local apisix_kms      = require("apisix.kms")
 local set_upstream    = apisix_upstream.set_by_route
 local apisix_ssl      = require("apisix.ssl")
 local upstream_util   = require("apisix.utils.upstream")
@@ -150,6 +151,7 @@ function _M.http_init_worker()
     plugin_config.init_worker()
     require("apisix.consumer").init_worker()
     consumer_group.init_worker()
+    apisix_kms.init_worker()
 
     apisix_upstream.init_worker()
     require("apisix.plugins.ext-plugin.init").init_worker()
diff --git a/apisix/kms.lua b/apisix/kms.lua
new file mode 100644
index 000000000..2da234863
--- /dev/null
+++ b/apisix/kms.lua
@@ -0,0 +1,239 @@
+--
+-- 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.
+--
+
+local require   = require
+local core      = require("apisix.core")
+local string    = require("apisix.core.string")
+
+local find      = string.find
+local sub       = string.sub
+local upper     = string.upper
+local byte      = string.byte
+local type      = type
+local pcall     = pcall
+local pairs     = pairs
+local ipairs    = ipairs
+
+local _M = {}
+
+
+local KMS_PREFIX = "$KMS://"
+local kmss
+
+local function check_kms(conf)
+    local idx = find(conf.id or "", "/")
+    if not idx then
+        return false, "no kms id"
+    end
+    local service = sub(conf.id, 1, idx - 1)
+
+    local ok, kms_service = pcall(require, "apisix.kms." .. service)
+    if not ok then
+        return false, "kms service not exits, service: " .. service
+    end
+
+    return core.schema.check(kms_service.schema, conf)
+end
+
+
+local kms_lrucache = core.lrucache.new({
+    ttl = 300, count = 512
+})
+
+local function create_kms_kvs(values)
+    local kms_services = {}
+
+    for _, v in ipairs(values) do
+        local path = v.value.id
+        local idx = find(path, "/")
+        if not idx then
+            core.log.error("no kms id")
+            return nil
+        end
+
+        local service = sub(path, 1, idx - 1)
+        local id = sub(path, idx + 1)
+
+        if not kms_services[service] then
+            kms_services[service] = {}
+        end
+        kms_services[service][id] = v.value
+    end
+
+    return kms_services
+end
+
+
+ local function kms_kv(service, confid)
+    local kms_values
+    kms_values = core.config.fetch_created_obj("/kms")
+    if not kms_values or not kms_values.values then
+       return nil
+    end
+
+    local kms_services = kms_lrucache("kms_kv", kms_values.conf_version,
+            create_kms_kvs, kms_values.values)
+    return kms_services[service] and kms_services[service][confid]
+end
+
+
+function _M.kmss()
+    if not kmss then
+        return nil, nil
+    end
+
+    return kmss.values, kmss.conf_version
+end
+
+
+function _M.init_worker()
+    local cfg = {
+        automatic = true,
+        checker = check_kms,
+    }
+
+    kmss = core.config.new("/kms", cfg)
+end
+
+
+local function parse_kms_uri(kms_uri)
+    -- Avoid the error caused by has_prefix to cause a crash.
+    if type(kms_uri) ~= "string" then
+        return nil, "error kms_uri type: " .. type(kms_uri)
+    end
+
+    if not string.has_prefix(upper(kms_uri), KMS_PREFIX) then
+        return nil, "error kms_uri prefix: " .. kms_uri
+    end
+
+    local path = sub(kms_uri, #KMS_PREFIX + 1)
+    local idx1 = find(path, "/")
+    if not idx1 then
+        return nil, "error format: no kms service"
+    end
+    local service = sub(path, 1, idx1 - 1)
+
+    local idx2 = find(path, "/", idx1 + 1)
+    if not idx2 then
+        return nil, "error format: no kms conf id"
+    end
+    local confid = sub(path, idx1 + 1, idx2 - 1)
+
+    local key = sub(path, idx2 + 1)
+    if key == "" then
+        return nil, "error format: no kms key id"
+    end
+
+    local opts = {
+        service = service,
+        confid = confid,
+        key = key
+    }
+    return opts
+end
+
+
+local function fetch_by_uri(kms_uri)
+    local opts, err = parse_kms_uri(kms_uri)
+    if not opts then
+        return nil, err
+    end
+
+    local conf = kms_kv(opts.service, opts.confid)
+    if not conf then
+        return nil, "no kms conf, kms_uri: " .. kms_uri
+    end
+
+    local ok, sm = pcall(require, "apisix.kms." .. opts.service)
+    if not ok then
+        return nil, "no kms service: " .. opts.service
+    end
+
+    local value, err = sm.get(conf, opts.key)
+    if err then
+        return nil, err
+    end
+
+    return value
+end
+
+-- for test
+_M.fetch_by_uri = fetch_by_uri
+
+
+local function fetch(uri)
+    -- do a quick filter to improve retrieval speed
+    if byte(uri, 1, 1) ~= byte('$') then
+        return nil
+    end
+
+    local val, err
+    if string.has_prefix(upper(uri), core.env.PREFIX) then
+        val, err = core.env.fetch_by_uri(uri)
+    elseif string.has_prefix(upper(uri), KMS_PREFIX) then
+        val, err = fetch_by_uri(uri)
+    end
+
+    if err then
+        core.log.error("failed to fetch kms value: ", err)
+        return
+    end
+
+    return val
+end
+
+
+local secrets_lrucache = core.lrucache.new({
+    ttl = 300, count = 512
+})
+
+local fetch_secrets
+do
+    local retrieve_refs
+    function retrieve_refs(refs)
+        for k, v in pairs(refs) do
+            local typ = type(v)
+            if typ == "string" then
+                refs[k] = fetch(v) or v
+            elseif typ == "table" then
+                retrieve_refs(v)
+            end
+        end
+        return refs
+    end
+
+    local function retrieve(refs)
+        core.log.info("retrieve secrets refs")
+
+        local new_refs = core.table.deepcopy(refs)
+        return retrieve_refs(new_refs)
+    end
+
+    function fetch_secrets(refs, cache, key, version)
+        if not refs or type(refs) ~= "table" then
+            return nil
+        end
+        if not cache then
+            return retrieve(refs)
+        end
+        return secrets_lrucache(key, version, retrieve, refs)
+    end
+end
+
+_M.fetch_secrets = fetch_secrets
+
+return _M
diff --git a/apisix/kms/vault.lua b/apisix/kms/vault.lua
index 1343002fc..41111e2b7 100644
--- a/apisix/kms/vault.lua
+++ b/apisix/kms/vault.lua
@@ -18,15 +18,32 @@
 --- Vault Tools.
 --  Vault is an identity-based secrets and encryption management system.
 
-local core = require("apisix.core")
-local http = require("resty.http")
+local core       = require("apisix.core")
+local http       = require("resty.http")
 
 local norm_path = require("pl.path").normpath
 
 local sub        = core.string.sub
 local rfind_char = core.string.rfind_char
 
-local _M = {}
+
+local schema = {
+    type = "object",
+    properties = {
+        uri = core.schema.uri_def,
+        prefix = {
+            type = "string",
+        },
+        token = {
+            type = "string",
+        },
+    },
+    required = {"uri", "prefix", "token"},
+}
+
+local _M = {
+    schema = schema
+}
 
 
 local function make_request_to_vault(conf, method, key, data)
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index 0bdb56ecd..f7b117af9 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -692,21 +692,6 @@ _M.service = {
 }
 
 
-_M.kms_vault = {
-    type = "object",
-    properties = {
-        uri = _M.uri_def,
-        prefix = {
-            type = "string",
-        },
-        token = {
-            type = "string",
-        },
-    },
-    required = {"uri", "prefix", "token"},
-}
-
-
 _M.consumer = {
     type = "object",
     properties = {
diff --git a/docs/assets/images/kms.png b/docs/assets/images/kms.png
new file mode 100644
index 000000000..54c10e9ec
Binary files /dev/null and b/docs/assets/images/kms.png differ
diff --git a/docs/en/latest/terminology/kms.md b/docs/en/latest/terminology/kms.md
index 56754bf84..7a7fac0a9 100644
--- a/docs/en/latest/terminology/kms.md
+++ b/docs/en/latest/terminology/kms.md
@@ -32,7 +32,13 @@ Secrets refer to any sensitive information required during the running process o
 
 KMS allows users to store Secrets through some secrets management services (Vault, etc.) in APISIX, and read them according to the key when using them to ensure that **Secrets do not exist in plain text throughout the platform**.
 
-APISIX currently supports storing keys in environment variables.
+Its working principle is shown in the figure:
+![kms](../../../assets/images/kms.png)
+
+APISIX currently supports storing keys in the following ways:
+
+- [Environment Variables](#use-environment-variables-to-manage-secrets)
+- [HashiCorp Vault](#use-vault-to-manage-secrets)
 
 You use KMS functions by specifying format variables in the consumer configuration of the following plugins, such as `key-auth`.
 
@@ -42,9 +48,9 @@ If a configuration item is: `key: "$ENV://ABC"`, when the actual value correspon
 
 :::
 
-## Use environment variables to manage keys
+## Use environment variables to manage secrets
 
-Using environment variables to manage keys means that you can save key information in environment variables, and refer to environment variables through variables in a specific format when configuring plugins. APISIX supports referencing system environment variables and environment variables configured through the Nginx `env` directive.
+Using environment variables to manage secrets means that you can save key information in environment variables, and refer to environment variables through variables in a specific format when configuring plugins. APISIX supports referencing system environment variables and environment variables configured through the Nginx `env` directive.
 
 ### Usage
 
@@ -107,3 +113,65 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \
 ```
 
 Through the above steps, the `key` configuration in the `key-auth` plugin can be saved in the environment variable instead of being displayed in plain text when configuring the plugin.
+
+## Use Vault to manage secrets
+
+Using Vault to manage secrets means that you can store secrets information in the Vault service and refer to it through variables in a specific format when configuring plugins. APISIX currently supports [Vault KV engine version V1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1).
+
+### Usage
+
+```
+$KMS://$secretmanager/$id/$secret_id/$key
+```
+
+- secretmanager: secrets management service, could be the Vault, AWS, etc.
+- id: KMS resource ID, which needs to be consistent with the one specified when adding the KMS resource
+- secret_id: the secret ID in the secrets management service
+- key: the key corresponding to the secret in the secrets management service
+
+### Example: use in key-auth plugin
+
+Step 1: Create the corresponding key in the Vault, you can use the following command:
+
+```shell
+vault kv put apisix/jack auth-key=value
+```
+
+Step 2: Add KMS resources through the Admin API, configure the Vault address and other connection information:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/kms/vault/1 \
+-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "https://127.0.0.1:8200",
+    "prefix": "apisix",
+    "token": "root"
+}'
+```
+
+If you use APISIX Standalone mode, you can add the following configuration in `apisix.yaml` configuration file:
+
+```yaml
+kms:
+  - id: vault/1
+    prefix: apisix
+    token: root
+    uri: 127.0.0.1:8200
+```
+
+Step 3: Reference the KMS resource in the `key-auth` plugin and fill in the key information:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/consumers \
+-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "username": "jack",
+    "plugins": {
+        "key-auth": {
+            "key": "$KMS://vault/1/jack/auth-key"
+        }
+    }
+}'
+```
+
+Through the above two steps, when the user request hits the `key-auth` plugin, the real value of the key in the Vault will be obtained through the KMS component.
diff --git a/docs/zh/latest/terminology/kms.md b/docs/zh/latest/terminology/kms.md
index 6ab4a8ca7..15404fbf3 100644
--- a/docs/zh/latest/terminology/kms.md
+++ b/docs/zh/latest/terminology/kms.md
@@ -32,7 +32,13 @@ Secrets 是指 APISIX 运行过程中所需的任何敏感信息,它可能是
 
 KMS 允许用户在 APISIX 中通过一些密钥管理服务(Vault 等)来存储 Secrets,在使用的时候根据 key 进行读取,确保 Secrets 在整个平台中不以明文的形式存在。
 
-APISIX 目前支持将密钥存储在环境变量中。
+其工作原理如图所示:
+![kms](../../../assets/images/kms.png)
+
+APISIX 目前支持通过以下方式存储密钥:
+
+- [环境变量](#使用环境变量管理密钥)
+- [HashiCorp Vault](#使用-vault-管理密钥)
 
 你可以在以下插件的 consumer 配置中通过指定格式的变量来使用 KMS 功能,比如 `key-auth` 插件。
 
@@ -107,3 +113,65 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \
 ```
 
 通过以上步骤,可以将 `key-auth` 插件中的 key 配置保存在环境变量中,而不是在配置插件时明文显示。
+
+## 使用 Vault 管理密钥
+
+使用 Vault 来管理密钥意味着你可以将密钥信息保存在 Vault 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 [Vault KV 引擎的 V1 版本](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1)。
+
+### 引用方式
+
+```
+$KMS://$secretmanager/$id/$secret_id/$key
+```
+
+- secretmanager: 密钥管理服务,可以是 Vault、AWS 等
+- id:KMS 资源 ID, 需要与添加 KMS 资源时指定的 ID 保持一致
+- secret_id: 密钥管理服务中的密钥 ID
+- key: 密钥管理服务中密钥对应的 key
+
+### 示例:在 key-auth 插件中使用
+
+第一步:在 Vault 中创建对应的密钥,可以使用如下命令:
+
+```shell
+vault kv put apisix/jack auth-key=value
+```
+
+第二步:通过 Admin API 添加 KMS 资源,配置 Vault 的地址等连接信息:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/kms/vault/1 \
+-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "https://127.0.0.1:8200",
+    "prefix": "apisix",
+    "token": "root"
+}'
+```
+
+如果使用 APISIX Standalone 版本,则可以在 `apisix.yaml`  文件中添加如下配置:
+
+```yaml
+kms:
+  - id: vault/1
+    prefix: apisix
+    token: root
+    uri: 127.0.0.1:8200
+```
+
+第三步:在 `key-auth` 插件中引用 KMS 资源,填充秘钥信息:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/consumers \
+-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "username": "jack",
+    "plugins": {
+        "key-auth": {
+            "key": "$KMS://vault/1/jack/auth-key"
+        }
+    }
+}'
+```
+
+通过上面两步操作,当用户请求命中 `key-auth` 插件时,会通过 KMS 组件获取到 key 在 Vault 中的真实值。
diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t
new file mode 100644
index 000000000..84b2c4c77
--- /dev/null
+++ b/t/config-center-yaml/kms.t
@@ -0,0 +1,413 @@
+#
+# 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);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    my $yaml_config = $block->yaml_config // <<_EOC_;
+apisix:
+    node_listen: 1984
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+_EOC_
+
+    $block->set_value("yaml_config", $yaml_config);
+
+    if (!$block->apisix_yaml) {
+        my $routes = <<_EOC_;
+routes:
+  -
+    uri: /hello
+    upstream:
+        nodes:
+            "127.0.0.1:1980": 1
+        type: roundrobin
+#END
+_EOC_
+
+        $block->set_value("apisix_yaml", $routes);
+    }
+
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: validate kms/vault: wrong schema
+--- apisix_yaml
+kms:
+  - id: vault/1
+    prefix: kv/apisix
+    token: root
+    uri: 127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local values = kms.kmss()
+            ngx.say(#values)
+        }
+    }
+--- request
+GET /t
+--- response_body
+0
+--- error_log
+property "uri" validation failed: failed to match pattern "^[^\\/]+:\\/\\/([\\da-zA-Z.-]+|\\[[\\da-fA-F:]+\\])(:\\d+)?"
+
+
+
+=== TEST 2: validate kms: service not exits
+--- apisix_yaml
+kms:
+  - id: hhh/1
+    prefix: kv/apisix
+    token: root
+    uri: 127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local values = kms.kmss()
+            ngx.say(#values)
+        }
+    }
+--- request
+GET /t
+--- response_body
+0
+--- error_log
+kms service not exits
+
+
+
+=== TEST 3: load config normal
+--- apisix_yaml
+kms:
+  - id: vault/1
+    prefix: kv/apisix
+    token: root
+    uri: http://127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local values = kms.kmss()
+            ngx.say("len: ", #values)
+
+            ngx.say("id: ", values[1].value.id)
+            ngx.say("prefix: ", values[1].value.prefix)
+            ngx.say("token: ", values[1].value.token)
+            ngx.say("uri: ", values[1].value.uri)
+        }
+    }
+--- request
+GET /t
+--- response_body
+len: 1
+id: vault/1
+prefix: kv/apisix
+token: root
+uri: http://127.0.0.1:8200
+
+
+
+=== TEST 4: store secret into vault
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key key=value
+--- response_body
+Success! Data written to: kv/apisix/apisix-key
+
+
+
+=== TEST 5: kms.fetch_by_uri: start with $kms://
+--- apisix_yaml
+kms:
+  - id: vault/1
+    prefix: kv/apisix
+    token: root
+    uri: http://127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local value = kms.fetch_by_uri("$kms://vault/1/apisix-key/key")
+            ngx.say(value)
+        }
+    }
+--- request
+GET /t
+--- response_body
+value
+
+
+
+=== TEST 6: kms.fetch_by_uri: start with $KMS://
+--- apisix_yaml
+kms:
+  - id: vault/1
+    prefix: kv/apisix
+    token: root
+    uri: http://127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local value = kms.fetch_by_uri("$KMS://vault/1/apisix-key/key")
+            ngx.say(value)
+        }
+    }
+--- request
+GET /t
+--- response_body
+value
+
+
+
+=== TEST 7: kms.fetch_by_uri, wrong ref format: wrong type
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri(1)
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error kms_uri type: number
+
+
+
+=== TEST 8: kms.fetch_by_uri, wrong ref format: wrong prefix
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri("kms://")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error kms_uri prefix: kms://
+
+
+
+=== TEST 9: kms.fetch_by_uri, error format: no kms service
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri("$kms://")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no kms service
+
+
+
+=== TEST 10: kms.fetch_by_uri, error format: no kms conf id
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri("$kms://vault/")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no kms conf id
+
+
+
+=== TEST 11: kms.fetch_by_uri, error format: no kms key id
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri("$kms://vault/2/")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no kms key id
+
+
+
+=== TEST 12: kms.fetch_by_uri, no config
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local _, err = kms.fetch_by_uri("$kms://vault/2/bar")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+no kms conf, kms_uri: $kms://vault/2/bar
+
+
+
+=== TEST 13: kms.fetch_by_uri, no sub key value
+--- apisix_yaml
+kms:
+  - id: vault/1
+    prefix: kv/apisix
+    token: root
+    uri: http://127.0.0.1:8200
+#END
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local value = kms.fetch_by_uri("$kms://vault/1/apisix-key/bar")
+            ngx.say(value)
+        }
+    }
+--- request
+GET /t
+--- response_body
+nil
+
+
+
+=== TEST 14: fetch_secrets env: no cache
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local refs = {
+                key = "jack",
+                secret = "$env://secret"
+            }
+            local new_refs = kms.fetch_secrets(refs)
+            assert(new_refs ~= refs)
+            ngx.say(refs.secret)
+            ngx.say(new_refs.secret)
+            ngx.say(new_refs.key)
+        }
+    }
+--- request
+GET /t
+--- response_body
+$env://secret
+apisix
+jack
+--- error_log_like
+qr/retrieve secrets refs/
+
+
+
+=== TEST 15: fetch_secrets env: cache
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local refs = {
+                key = "jack",
+                secret = "$env://secret"
+            }
+            local refs_1 = kms.fetch_secrets(refs, true, "key", 1)
+            local refs_2 = kms.fetch_secrets(refs, true, "key", 1)
+            assert(refs_1 == refs_2)
+            ngx.say(refs_1.secret)
+            ngx.say(refs_2.secret)
+        }
+    }
+--- request
+GET /t
+--- response_body
+apisix
+apisix
+--- grep_error_log eval
+qr/retrieve secrets refs/
+--- grep_error_log_out
+retrieve secrets refs
+
+
+
+=== TEST 16: fetch_secrets env: table nesting
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local refs = {
+                key = "jack",
+                user = {
+                    username = "apisix",
+                    passsword = "$env://secret"
+                }
+            }
+            local new_refs = kms.fetch_secrets(refs)
+            ngx.say(new_refs.user.passsword)
+        }
+    }
+--- request
+GET /t
+--- response_body
+apisix
+
+
+
+=== TEST 17: fetch_secrets: wrong refs type
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local kms = require("apisix.kms")
+            local refs = "wrong"
+            local new_refs = kms.fetch_secrets(refs)
+            ngx.say(new_refs)
+        }
+    }
+--- request
+GET /t
+--- response_body
+nil
diff --git a/t/core/env.t b/t/core/env.t
index 745fcef1a..2e14a4397 100644
--- a/t/core/env.t
+++ b/t/core/env.t
@@ -34,7 +34,7 @@ __DATA__
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$env://TEST_ENV_VAR")
+            local value = env.fetch_by_uri("$env://TEST_ENV_VAR")
             ngx.say(value)
         }
     }
@@ -50,7 +50,7 @@ test-value
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://TEST_ENV_VAR")
+            local value = env.fetch_by_uri("$ENV://TEST_ENV_VAR")
             ngx.say(value)
         }
     }
@@ -66,7 +66,7 @@ test-value
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://test_env_var")
+            local value = env.fetch_by_uri("$ENV://test_env_var")
             ngx.say(value)
         }
     }
@@ -82,18 +82,18 @@ nil
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get(1)
-            ngx.say(value)
+            local _, err = env.fetch_by_uri(1)
+            ngx.say(err)
 
-            local value = env.get(true)
-            ngx.say(value)
+            local _, err = env.fetch_by_uri(true)
+            ngx.say(err)
         }
     }
 --- request
 GET /t
 --- response_body
-nil
-nil
+error env_uri type: number
+error env_uri type: boolean
 
 
 
@@ -102,14 +102,14 @@ nil
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("env://")
-            ngx.say(value)
+            local _, err = env.fetch_by_uri("env://")
+            ngx.say(err)
         }
     }
 --- request
 GET /t
 --- response_body
-nil
+error env_uri prefix: env://
 
 
 
@@ -118,9 +118,9 @@ nil
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://TEST_ENV_SUB_VAR/main")
+            local value = env.fetch_by_uri("$ENV://TEST_ENV_SUB_VAR/main")
             ngx.say(value)
-            local value = env.get("$ENV://TEST_ENV_SUB_VAR/sub")
+            local value = env.fetch_by_uri("$ENV://TEST_ENV_SUB_VAR/sub")
             ngx.say(value)
         }
     }
@@ -137,14 +137,14 @@ sub_value
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://TEST_ENV_VAR/main")
-            ngx.say(value)
+            local _, err = env.fetch_by_uri("$ENV://TEST_ENV_VAR/main")
+            ngx.say(err)
         }
     }
 --- request
 GET /t
 --- response_body
-nil
+decode failed, err: Expected value but found invalid token at character 1, value: test-value
 
 
 
@@ -153,7 +153,7 @@ nil
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://TEST_ENV_VAR/no")
+            local value = env.fetch_by_uri("$ENV://TEST_ENV_VAR/no")
             ngx.say(value)
         }
     }
@@ -171,7 +171,7 @@ env ngx_env=apisix-nice;
     location /t {
         content_by_lua_block {
             local env = require("apisix.core.env")
-            local value = env.get("$ENV://ngx_env")
+            local value = env.fetch_by_uri("$ENV://ngx_env")
             ngx.say(value)
         }
     }
diff --git a/t/core/utils.t b/t/core/utils.t
index 9094ddc14..4e6b0d766 100644
--- a/t/core/utils.t
+++ b/t/core/utils.t
@@ -361,114 +361,3 @@ apisix:
 GET /t
 --- error_log
 failed to parse domain: ipv6.local
-
-
-
-=== TEST 12: retrieve_secrets_ref: no cache
---- main_config
-env secret=apisix;
---- extra_init_by_lua
-require("apisix.core.env").init()
---- config
-    location /t {
-        content_by_lua_block {
-            local core = require("apisix.core")
-            local refs = {
-                key = "jack",
-                secret = "$env://secret"
-            }
-            local new_refs = core.utils.retrieve_secrets_ref(refs)
-            assert(new_refs ~= refs)
-            ngx.say(refs.secret)
-            ngx.say(new_refs.secret)
-            ngx.say(new_refs.key)
-        }
-    }
---- request
-GET /t
---- response_body
-$env://secret
-apisix
-jack
---- error_log_like
-qr/retrieve secrets refs/
-
-
-
-=== TEST 13: retrieve_secrets_ref: cache
---- main_config
-env secret=apisix;
---- extra_init_by_lua
-require("apisix.core.env").init()
---- config
-    location /t {
-        content_by_lua_block {
-            local core = require("apisix.core")
-            local refs = {
-                key = "jack",
-                secret = "$env://secret"
-            }
-            local refs_1 = core.utils.retrieve_secrets_ref(refs, true, "key", 1)
-            local refs_2 = core.utils.retrieve_secrets_ref(refs, true, "key", 1)
-            assert(refs_1 == refs_2)
-            ngx.say(refs_1.secret)
-            ngx.say(refs_2.secret)
-        }
-    }
---- request
-GET /t
---- response_body
-apisix
-apisix
---- grep_error_log eval
-qr/retrieve secrets refs/
---- grep_error_log_out
-retrieve secrets refs
-
-
-
-=== TEST 14: retrieve_secrets_ref: table nesting
---- main_config
-env secret=apisix;
---- extra_init_by_lua
-require("apisix.core.env").init()
---- config
-    location /t {
-        content_by_lua_block {
-            local core = require("apisix.core")
-            local refs = {
-                key = "jack",
-                user = {
-                    username = "apisix",
-                    passsword = "$env://secret"
-                }
-            }
-            local new_refs = core.utils.retrieve_secrets_ref(refs)
-            ngx.say(new_refs.user.passsword)
-        }
-    }
---- request
-GET /t
---- response_body
-apisix
-
-
-
-=== TEST 15: retrieve_secrets_ref: wrong refs type
---- main_config
-env secret=apisix;
---- extra_init_by_lua
-require("apisix.core.env").init()
---- config
-    location /t {
-        content_by_lua_block {
-            local core = require("apisix.core")
-            local refs = "wrong"
-            local new_refs = core.utils.retrieve_secrets_ref(refs)
-            ngx.say(new_refs)
-        }
-    }
---- request
-GET /t
---- response_body
-nil
diff --git a/t/plugin/key-auth.t b/t/plugin/key-auth.t
index 4f139bbfe..c9f78bb43 100644
--- a/t/plugin/key-auth.t
+++ b/t/plugin/key-auth.t
@@ -538,7 +538,7 @@ auth: auth-one
 
 
 
-=== TEST 26: change consumer with secrets ref
+=== TEST 26: change consumer with secrets ref: env
 --- config
     location /t {
         content_by_lua_block {
@@ -568,10 +568,90 @@ passed
 
 
 
-=== TEST 27: verify auth request args should not hidden
+=== TEST 27: verify auth request
 --- main_config
 env test_auth=authone;
 --- request
 GET /hello?auth=authone
 --- response_args
 auth: authone
+
+
+
+=== TEST 28: put kms vault config
+--- request
+GET /t
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local etcd = require("apisix.core.etcd")
+            local code, body = t('/apisix/admin/kms/vault/test1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "http://127.0.0.1:8200",
+                    "prefix" : "kv/apisix",
+                    "token" : "root"
+                }]],
+                [[{
+                    "value": {
+                        "uri": "http://127.0.0.1:8200",
+                        "prefix" : "kv/apisix",
+                        "token" : "root"
+                    },
+                    "key": "/apisix/kms/vault/test1"
+                }]]
+                )
+
+            ngx.status = code
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 29: change consumer with secrets ref: vault
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "jack",
+                    "plugins": {
+                        "key-auth": {
+                            "key": "$kms://vault/test1/jack/auth-key"
+                        }
+                    }
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 30: store secret into vault
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jack auth-key=authtwo
+--- response_body
+Success! Data written to: kv/apisix/jack
+
+
+
+=== TEST 31: verify auth request
+--- request
+GET /hello?auth=authtwo
+--- response_args
+auth: authtwo