From 309312af7fb96d0ee09f4e54edf58470ae6d5fc7 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 14:38:39 +0800 Subject: [PATCH 01/23] feat: Support the use of kms resource configuration and management of kms components --- apisix/constants.lua | 1 + apisix/init.lua | 2 + apisix/kms.lua | 181 ++++++++++++++++++++ apisix/schema_def.lua | 15 ++ t/config-center-yaml/kms.t | 326 +++++++++++++++++++++++++++++++++++++ 5 files changed, 525 insertions(+) create mode 100644 apisix/kms.lua create mode 100644 t/config-center-yaml/kms.t diff --git a/apisix/constants.lua b/apisix/constants.lua index 9f9b62fc3ba0..40ab71a2b414 100644 --- a/apisix/constants.lua +++ b/apisix/constants.lua @@ -33,6 +33,7 @@ return { ["/protos"] = true, ["/plugin_configs"] = true, ["/consumer_groups"] = true, + ["/kms"] = true, }, STREAM_ETCD_DIRECTORY = { ["/upstreams"] = true, diff --git a/apisix/init.lua b/apisix/init.lua index fbb090bee3f6..038eb578b182 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 000000000000..cf993677a023 --- /dev/null +++ b/apisix/kms.lua @@ -0,0 +1,181 @@ +-- +-- 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 type = type + +local _M = { + version = 0.1, +} + + +local KMS_PREFIX = "$KMS://" +local kmss + +local function check_kms(conf) + --core.log.warn("check path: ", path, " :", require("inspect")(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 = pcall(require, "apisix.kms." .. service) + if not ok then + return false, "kms service not exits, service: " .. service + end + + return core.schema.check(core.schema["kms_" .. service], conf) +end + + +local 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 + 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 then + return nil + end + --core.log.warn("check path: ", confid, " :", require("inspect")(kms_kv)) + local kms_services = lrucache("kms_kv", kms_values.conf_version, create_kms_kvs, kms_values.values) + --core.log.warn("hhget: ", require("inspect")(kms_services)) + return kms_services[service] and kms_services[service][confid] or nil +end + + +function _M.kmss() + if not kmss then + return nil, nil + end + + return kmss.values, kmss.conf_version +end + + +function _M.init_worker() + local err + local cfg = { + automatic = true, + checker = check_kms, + } + + kmss, err = core.config.new("/kms", cfg) + if not kmss then + error("failed to create etcd instance for fetching kmss: " .. err) + return + end +end + + +local function is_kms_uri(kms_uri) + -- Avoid the error caused by has_prefix to cause a crash. + return type(kms_uri) == "string" and + string.has_prefix(upper(kms_uri), KMS_PREFIX) +end + + +local function parse_kms_uri(kms_uri) + 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, nil +end + + +function _M.get(kms_uri) + if not is_kms_uri(kms_uri) then + return nil + end + local opts, err = parse_kms_uri(kms_uri) + if not opts then + core.log.warn(err) + return nil + end + local conf = kms_kv(opts.service, opts.confid) + if not conf then + core.log.error("no config") + return nil + end + local sm = require("apisix.kms." .. opts.service) + if not sm then + core.log.error("no kms service: ", opts.service) + return nil + end + local value, err = sm.get(conf, opts.key) + if err then + core.log.error(err) + return nil + end + return value +end + + +return _M diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index f7b117af93da..0bdb56ecd0e0 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -692,6 +692,21 @@ _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/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t new file mode 100644 index 000000000000..d1f6fe7d343a --- /dev/null +++ b/t/config-center-yaml/kms.t @@ -0,0 +1,326 @@ +# +# 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); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: validate kms/vault: wrong schema +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/prefix + token: hvs.GD4458NcXuKqOdEUaaAiuKiR + 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: apisix-key + service: hhh + prefix: kv/prefix + token: hvs.GD4458NcXuKqOdEUaaAiuKiR + 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: normal +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/prefix + 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) + ngx.say("service: ", values[1].value.service) + } + } +--- request +GET /t +--- response_body +len: 1 +id: apisix-key/vault +prefix: kv/prefix +token: root +uri: http://127.0.0.1:8200 +service: nil + + + +=== 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/bar key=value +--- response_body +Success! Data written to: kv/apisix/apisix-key/bar + + + +=== TEST 5: kms.get: start with $kms:// +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://vault/apisix-key/bar/key") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 6: kms.get: start with $KMS:// +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$KMS://vault/apisix-key/bar/key") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +value + + + +=== TEST 7: kms.get, wrong ref format: wrong type +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get(1) + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil + + + +=== TEST 8: kms.get, wrong ref format: wrong prefix +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("kms://") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil + + + +=== TEST 9: kms.get, error format: no kms service +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil +--- error_log +error format: no kms service + + + +=== TEST 10: kms.get, error format: no kms conf id +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://vault/") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil +--- error_log +error format: no kms conf id + + + +=== TEST 11: kms.get, error format: no kms key id +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://vault/1/") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil +--- error_log +error format: no kms key id + + + +=== TEST 12: kms.get, no config +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://vault/1/bar") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil +--- error_log +no config + + + +=== TEST 13: kms.get, no kms service +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/prefix + token: root + uri: 127.0.0.1:8200 +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://dummy/1/bar") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil +--- error_log +no config + + + +=== TEST 14: kms.get, no sub key +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +--- config + location /t { + content_by_lua_block { + local kms = require("apisix.kms") + local value = kms.get("$kms://vault/apisix-key/bar/test") + ngx.say(value) + } + } +--- request +GET /t +--- response_body +nil From fe2f8d08b2158dc32ee01200862803591f81833b Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 14:41:42 +0800 Subject: [PATCH 02/23] remove comment --- apisix/kms.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index cf993677a023..3765d0432baa 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -33,8 +33,6 @@ local KMS_PREFIX = "$KMS://" local kmss local function check_kms(conf) - --core.log.warn("check path: ", path, " :", require("inspect")(conf)) - local idx = find(conf.id or "", "/") if not idx then return false, "no kms id" @@ -84,9 +82,8 @@ end if not kms_values then return nil end - --core.log.warn("check path: ", confid, " :", require("inspect")(kms_kv)) + local kms_services = lrucache("kms_kv", kms_values.conf_version, create_kms_kvs, kms_values.values) - --core.log.warn("hhget: ", require("inspect")(kms_services)) return kms_services[service] and kms_services[service][confid] or nil end From 7462913635792e82354ff8d18fd07d1d69a4dfe2 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 14:45:00 +0800 Subject: [PATCH 03/23] fix lint --- apisix/kms.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index 3765d0432baa..e9fa50aa382f 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -83,7 +83,8 @@ end return nil end - local kms_services = lrucache("kms_kv", kms_values.conf_version, create_kms_kvs, kms_values.values) + local kms_services = lrucache("kms_kv", kms_values.conf_version, + create_kms_kvs, kms_values.values) return kms_services[service] and kms_services[service][confid] or nil end From 29fca2955b08a7969ff272a960e396c377cb9237 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 14:48:16 +0800 Subject: [PATCH 04/23] fix lint --- apisix/kms.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index e9fa50aa382f..b6132881352b 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -19,10 +19,13 @@ 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 type = type +local find = string.find +local sub = string.sub +local upper = string.upper +local type = type +local pcall = pcall +local ipairs = ipairs +local error = error local _M = { version = 0.1, From 0bd4987d80b29aaed41bb80adde5627d14cb643d Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 14:50:55 +0800 Subject: [PATCH 05/23] fix lint --- t/config-center-yaml/kms.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index d1f6fe7d343a..8ee20173a531 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -65,7 +65,7 @@ property "uri" validation failed: failed to match pattern "^[^\\/]+:\\/\\/([\\da -=== TEST 2: validate kms: service not exits +=== TEST 2: validate kms: service not exits --- apisix_yaml kms: - id: apisix-key From e14f7b3573330e2bb5b4bc51e5c76b03faaa086f Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 15:45:44 +0800 Subject: [PATCH 06/23] fix ci --- apisix/kms.lua | 2 +- t/config-center-yaml/kms.t | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index b6132881352b..c145775a052f 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -82,7 +82,7 @@ end local function kms_kv(service, confid) local kms_values kms_values = core.config.fetch_created_obj("/kms") - if not kms_values then + if not kms_values or not kms_values.values then return nil end diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index 8ee20173a531..f2030b284603 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -45,7 +45,7 @@ __DATA__ kms: - id: vault/apisix-key prefix: kv/prefix - token: hvs.GD4458NcXuKqOdEUaaAiuKiR + token: root uri: 127.0.0.1:8200 #END --- config From 184e70de1fc988299ca56c4c46b7cf6ef630c985 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 17:04:53 +0800 Subject: [PATCH 07/23] using kms in auth plugin --- apisix/consumer.lua | 9 +++- apisix/core/utils.lua | 20 ++++---- docs/assets/images/kms.png | Bin 0 -> 125602 bytes docs/en/latest/terminology/kms.md | 74 +++++++++++++++++++++++++-- docs/zh/latest/terminology/kms.md | 70 ++++++++++++++++++++++++- t/core/utils.t | 10 ++-- t/plugin/key-auth.t | 82 +++++++++++++++++++++++++++++- 7 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 docs/assets/images/kms.png diff --git a/apisix/consumer.lua b/apisix/consumer.lua index 32ff69275809..5e2c3db4733e 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 @@ -97,13 +98,19 @@ function _M.consumers() end +local function retrieve_secrets_callback(key) + return core.env.get(key) or kms.get(key) +end + + local function create_consume_cache(consumers_conf, key_attr) local consumer_names = {} 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 = + core.utils.retrieve_secrets_ref(new_consumer.auth_conf, retrieve_secrets_callback) consumer_names[new_consumer.auth_conf[key_attr]] = new_consumer end diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua index 6b61cf02080e..c6946efac7c3 100644 --- a/apisix/core/utils.lua +++ b/apisix/core/utils.lua @@ -25,7 +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") @@ -339,42 +338,43 @@ local secrets_lrucache = lrucache.new({ local retrieve_secrets_ref do local retrieve_ref - function retrieve_ref(refs) + function retrieve_ref(refs, callback) for k, v in pairs(refs) do local typ = type(v) if typ == "string" then - refs[k] = env.get(v) or v + refs[k] = callback(v) or v elseif typ == "table" then - retrieve_ref(v) + retrieve_ref(v, callback) end end return refs end - local function retrieve(refs) + local function retrieve(refs, callback) log.info("retrieve secrets refs") local new_refs = table.deepcopy(refs) - return retrieve_ref(new_refs) + return retrieve_ref(new_refs, callback) end - function retrieve_secrets_ref(refs, cache, key, version) + function retrieve_secrets_ref(refs, callback, cache, key, version) if not refs or type(refs) ~= "table" then return nil end if not cache then - return retrieve(refs) + return retrieve(refs, callback) end - return secrets_lrucache(key, version, retrieve, refs) + return secrets_lrucache(key, version, retrieve, refs, callback) 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. +-- and then replace them with the values from the environment variables or kms. -- -- @function core.utils.retrieve_secrets_ref -- @tparam table refs The table to be retrieved. +-- @tparam function callback The replacement function to use when iterating over values. -- @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. diff --git a/docs/assets/images/kms.png b/docs/assets/images/kms.png new file mode 100644 index 0000000000000000000000000000000000000000..54c10e9ecaf80929df93c7d2bfe1c4bdc0cd8a07 GIT binary patch literal 125602 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 literal 0 HcmV?d00001 diff --git a/docs/en/latest/terminology/kms.md b/docs/en/latest/terminology/kms.md index 56754bf84511..e2602ccf76b0 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: + +``` +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: + +```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 6ab4a8ca76c9..e727236c52fd 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 资源时指定的保持一致 +- secret_id: 密钥管理服务中的密钥 id +- key: 密钥管理服务中密钥对应的 key + +### 示例:在 key-auth 插件中使用 + +第一步:在 Vault 中创建对应的密钥,可以使用如下命令: + +```bash +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/core/utils.t b/t/core/utils.t index 9094ddc1475a..979f08e51062 100644 --- a/t/core/utils.t +++ b/t/core/utils.t @@ -377,7 +377,7 @@ require("apisix.core.env").init() key = "jack", secret = "$env://secret" } - local new_refs = core.utils.retrieve_secrets_ref(refs) + local new_refs = core.utils.retrieve_secrets_ref(refs, core.env.get) assert(new_refs ~= refs) ngx.say(refs.secret) ngx.say(new_refs.secret) @@ -408,8 +408,8 @@ require("apisix.core.env").init() 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) + local refs_1 = core.utils.retrieve_secrets_ref(refs, core.env.get, true, "key", 1) + local refs_2 = core.utils.retrieve_secrets_ref(refs, core.env.get, true, "key", 1) assert(refs_1 == refs_2) ngx.say(refs_1.secret) ngx.say(refs_2.secret) @@ -443,7 +443,7 @@ require("apisix.core.env").init() passsword = "$env://secret" } } - local new_refs = core.utils.retrieve_secrets_ref(refs) + local new_refs = core.utils.retrieve_secrets_ref(refs, core.env.get) ngx.say(new_refs.user.passsword) } } @@ -464,7 +464,7 @@ require("apisix.core.env").init() content_by_lua_block { local core = require("apisix.core") local refs = "wrong" - local new_refs = core.utils.retrieve_secrets_ref(refs) + local new_refs = core.utils.retrieve_secrets_ref(refs, core.env.get) ngx.say(new_refs) } } diff --git a/t/plugin/key-auth.t b/t/plugin/key-auth.t index 4f139bbfeff6..8e4892bc0b5e 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,88 @@ 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 +--- 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 From 9cd63002cf6bcc956c5995d8e13877176a2b2fc5 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 17:35:10 +0800 Subject: [PATCH 08/23] add end flag for yaml --- t/config-center-yaml/kms.t | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index f2030b284603..86cb44305ba3 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -140,6 +140,7 @@ kms: prefix: kv/apisix token: root uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -162,6 +163,7 @@ kms: prefix: kv/apisix token: root uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -288,6 +290,7 @@ kms: prefix: kv/prefix token: root uri: 127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -312,6 +315,7 @@ kms: prefix: kv/apisix token: root uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { From 010f845eb8c716a5b5cee03cf2a3750acd08d00b Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 20:05:15 +0800 Subject: [PATCH 09/23] fix test case --- t/config-center-yaml/kms.t | 54 +++++++++++--------------------------- t/plugin/key-auth.t | 2 ++ 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index 86cb44305ba3..cd1b55005db7 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -34,6 +34,17 @@ deployment: _EOC_ $block->set_value("yaml_config", $yaml_config); + + my $kms = <<_EOC_; +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: 127.0.0.1:8200 +#END +_EOC_ + + $block->set_value("apisix_yaml", $block->apisix_yaml . $kms); }); run_tests(); @@ -44,7 +55,7 @@ __DATA__ --- apisix_yaml kms: - id: vault/apisix-key - prefix: kv/prefix + prefix: kv/apisix token: root uri: 127.0.0.1:8200 #END @@ -70,7 +81,7 @@ property "uri" validation failed: failed to match pattern "^[^\\/]+:\\/\\/([\\da kms: - id: apisix-key service: hhh - prefix: kv/prefix + prefix: kv/apisix token: hvs.GD4458NcXuKqOdEUaaAiuKiR uri: 127.0.0.1:8200 #END @@ -92,13 +103,6 @@ kms service not exits === TEST 3: normal ---- apisix_yaml -kms: - - id: vault/apisix-key - prefix: kv/prefix - token: root - uri: http://127.0.0.1:8200 -#END --- config location /t { content_by_lua_block { @@ -117,8 +121,8 @@ kms: GET /t --- response_body len: 1 -id: apisix-key/vault -prefix: kv/prefix +id: vault/apisix-key +prefix: kv/apisix token: root uri: http://127.0.0.1:8200 service: nil @@ -134,13 +138,6 @@ Success! Data written to: kv/apisix/apisix-key/bar === TEST 5: kms.get: start with $kms:// ---- apisix_yaml -kms: - - id: vault/apisix-key - prefix: kv/apisix - token: root - uri: http://127.0.0.1:8200 -#END --- config location /t { content_by_lua_block { @@ -157,13 +154,6 @@ value === TEST 6: kms.get: start with $KMS:// ---- apisix_yaml -kms: - - id: vault/apisix-key - prefix: kv/apisix - token: root - uri: http://127.0.0.1:8200 -#END --- config location /t { content_by_lua_block { @@ -284,13 +274,6 @@ no config === TEST 13: kms.get, no kms service ---- apisix_yaml -kms: - - id: vault/apisix-key - prefix: kv/prefix - token: root - uri: 127.0.0.1:8200 -#END --- config location /t { content_by_lua_block { @@ -309,13 +292,6 @@ no config === TEST 14: kms.get, no sub key ---- apisix_yaml -kms: - - id: vault/apisix-key - prefix: kv/apisix - token: root - uri: http://127.0.0.1:8200 -#END --- config location /t { content_by_lua_block { diff --git a/t/plugin/key-auth.t b/t/plugin/key-auth.t index 8e4892bc0b5e..c9f78bb4395f 100644 --- a/t/plugin/key-auth.t +++ b/t/plugin/key-auth.t @@ -579,6 +579,8 @@ auth: authone === TEST 28: put kms vault config +--- request +GET /t --- config location /t { content_by_lua_block { From 4b63ead76b4c0aad467d8ede51d0f8883c110c03 Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 29 Nov 2022 20:31:58 +0800 Subject: [PATCH 10/23] fix test case --- t/config-center-yaml/kms.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index cd1b55005db7..f35067ead0e4 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -57,7 +57,7 @@ kms: - id: vault/apisix-key prefix: kv/apisix token: root - uri: 127.0.0.1:8200 + uri: http://127.0.0.1:8200 #END --- config location /t { From 55874aa126c131bf335715db990a86103dc870a9 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 09:03:10 +0800 Subject: [PATCH 11/23] fix test case --- t/config-center-yaml/kms.t | 100 ++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index f35067ead0e4..d463eeab5009 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -35,18 +35,6 @@ _EOC_ $block->set_value("yaml_config", $yaml_config); - my $kms = <<_EOC_; -kms: - - id: vault/apisix-key - prefix: kv/apisix - token: root - uri: 127.0.0.1:8200 -#END -_EOC_ - - $block->set_value("apisix_yaml", $block->apisix_yaml . $kms); -}); - run_tests(); __DATA__ @@ -57,7 +45,7 @@ kms: - id: vault/apisix-key prefix: kv/apisix token: root - uri: http://127.0.0.1:8200 + uri: 127.0.0.1:8200 #END --- config location /t { @@ -79,10 +67,9 @@ property "uri" validation failed: failed to match pattern "^[^\\/]+:\\/\\/([\\da === TEST 2: validate kms: service not exits --- apisix_yaml kms: - - id: apisix-key - service: hhh + - id: hhh/apisix-key prefix: kv/apisix - token: hvs.GD4458NcXuKqOdEUaaAiuKiR + token: root uri: 127.0.0.1:8200 #END --- config @@ -103,6 +90,13 @@ kms service not exits === TEST 3: normal +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -114,7 +108,6 @@ kms service not exits ngx.say("prefix: ", values[1].value.prefix) ngx.say("token: ", values[1].value.token) ngx.say("uri: ", values[1].value.uri) - ngx.say("service: ", values[1].value.service) } } --- request @@ -125,7 +118,6 @@ id: vault/apisix-key prefix: kv/apisix token: root uri: http://127.0.0.1:8200 -service: nil @@ -138,6 +130,13 @@ Success! Data written to: kv/apisix/apisix-key/bar === TEST 5: kms.get: start with $kms:// +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -154,6 +153,13 @@ value === TEST 6: kms.get: start with $KMS:// +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -170,6 +176,13 @@ value === TEST 7: kms.get, wrong ref format: wrong type +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -186,6 +199,13 @@ nil === TEST 8: kms.get, wrong ref format: wrong prefix +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -202,6 +222,13 @@ nil === TEST 9: kms.get, error format: no kms service +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -220,6 +247,13 @@ error format: no kms service === TEST 10: kms.get, error format: no kms conf id +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -238,6 +272,13 @@ error format: no kms conf id === TEST 11: kms.get, error format: no kms key id +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -256,6 +297,13 @@ error format: no kms key id === TEST 12: kms.get, no config +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -274,6 +322,13 @@ no config === TEST 13: kms.get, no kms service +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { @@ -291,7 +346,14 @@ no config -=== TEST 14: kms.get, no sub key +=== TEST 14: kms.get, no sub key value +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- config location /t { content_by_lua_block { From f3c775db7059423ba54e58ecdb33c3a73ebbe004 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 09:35:39 +0800 Subject: [PATCH 12/23] fix synax --- t/config-center-yaml/kms.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index d463eeab5009..716f16ec4810 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -34,6 +34,7 @@ deployment: _EOC_ $block->set_value("yaml_config", $yaml_config); +}); run_tests(); From ca49f8c0f96201671e708c697f80ab194657b55f Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 10:00:17 +0800 Subject: [PATCH 13/23] fix test case --- t/config-center-yaml/kms.t | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index 716f16ec4810..8021cf485f51 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -123,6 +123,13 @@ uri: http://127.0.0.1:8200 === TEST 4: store secret into vault +--- apisix_yaml +kms: + - id: vault/apisix-key + prefix: kv/apisix + token: root + uri: http://127.0.0.1:8200 +#END --- exec VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key/bar key=value --- response_body From 45cf2c6069de2dbfccfe75458d6fbb063a8a3ff4 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 10:34:11 +0800 Subject: [PATCH 14/23] fix test case --- t/config-center-yaml/kms.t | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index 8021cf485f51..200693a68b06 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -43,7 +43,7 @@ __DATA__ === TEST 1: validate kms/vault: wrong schema --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: 127.0.0.1:8200 @@ -68,7 +68,7 @@ property "uri" validation failed: failed to match pattern "^[^\\/]+:\\/\\/([\\da === TEST 2: validate kms: service not exits --- apisix_yaml kms: - - id: hhh/apisix-key + - id: hhh/1 prefix: kv/apisix token: root uri: 127.0.0.1:8200 @@ -90,10 +90,10 @@ kms service not exits -=== TEST 3: normal +=== TEST 3: load config normal --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -115,7 +115,7 @@ kms: GET /t --- response_body len: 1 -id: vault/apisix-key +id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -125,22 +125,22 @@ uri: http://127.0.0.1:8200 === TEST 4: store secret into vault --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 #END --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key/bar key=value +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/bar +Success! Data written to: kv/apisix/apisix-key === TEST 5: kms.get: start with $kms:// --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -149,7 +149,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/apisix-key/bar/key") + local value = kms.get("$kms://vault/1/apisix-key/key") ngx.say(value) } } @@ -163,7 +163,7 @@ value === TEST 6: kms.get: start with $KMS:// --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -172,7 +172,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$KMS://vault/apisix-key/bar/key") + local value = kms.get("$KMS://vault/1/apisix-key/key") ngx.say(value) } } @@ -186,7 +186,7 @@ value === TEST 7: kms.get, wrong ref format: wrong type --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -209,7 +209,7 @@ nil === TEST 8: kms.get, wrong ref format: wrong prefix --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -232,7 +232,7 @@ nil === TEST 9: kms.get, error format: no kms service --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -257,7 +257,7 @@ error format: no kms service === TEST 10: kms.get, error format: no kms conf id --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -282,7 +282,7 @@ error format: no kms conf id === TEST 11: kms.get, error format: no kms key id --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -291,7 +291,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/1/") + local value = kms.get("$kms://vault/2/") ngx.say(value) } } @@ -307,7 +307,7 @@ error format: no kms key id === TEST 12: kms.get, no config --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -316,7 +316,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/1/bar") + local value = kms.get("$kms://vault/2/bar") ngx.say(value) } } @@ -357,7 +357,7 @@ no config === TEST 14: kms.get, no sub key value --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -366,7 +366,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/apisix-key/bar/test") + local value = kms.get("$kms://vault/1/apisix-key/bar") ngx.say(value) } } From 767219a1cd05808bbd0a5d85fb4032ad4e98c05b Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:03:36 +0800 Subject: [PATCH 15/23] adjust code structure --- apisix/admin/kms.lua | 11 ++- apisix/core/env.lua | 57 ++++++------ apisix/kms.lua | 114 +++++++++++++++++------ apisix/kms/vault.lua | 24 ++++- apisix/schema_def.lua | 15 ---- t/admin/kms.t | 1 + t/config-center-yaml/kms.t | 179 +++++++++++++++++++++++++++---------- t/core/env.t | 38 ++++---- t/core/utils.t | 111 ----------------------- 9 files changed, 301 insertions(+), 249 deletions(-) diff --git a/apisix/admin/kms.lua b/apisix/admin/kms.lua index 895b60421dc8..4be84a7e8fd1 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/core/env.lua b/apisix/core/env.lua index d8702904a68d..2bab04327ba9 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/kms.lua b/apisix/kms.lua index c145775a052f..962094e63347 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -22,14 +22,13 @@ 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 ipairs = ipairs local error = error -local _M = { - version = 0.1, -} +local _M = {} local KMS_PREFIX = "$KMS://" @@ -42,16 +41,16 @@ local function check_kms(conf) end local service = sub(conf.id, 1, idx - 1) - local ok = pcall(require, "apisix.kms." .. service) + 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(core.schema["kms_" .. service], conf) + return core.schema.check(kms_service.schema, conf) end -local lrucache = core.lrucache.new({ +local kms_lrucache = core.lrucache.new({ ttl = 300, count = 512 }) @@ -86,9 +85,9 @@ end return nil end - local kms_services = lrucache("kms_kv", kms_values.conf_version, + 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] or nil + return kms_services[service] and kms_services[service][confid] end @@ -116,14 +115,16 @@ function _M.init_worker() end -local function is_kms_uri(kms_uri) +local function parse_kms_uri(kms_uri) -- Avoid the error caused by has_prefix to cause a crash. - return type(kms_uri) == "string" and - string.has_prefix(upper(kms_uri), KMS_PREFIX) -end + 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 function parse_kms_uri(kms_uri) local path = sub(kms_uri, #KMS_PREFIX + 1) local idx1 = find(path, "/") if not idx1 then @@ -147,36 +148,97 @@ local function parse_kms_uri(kms_uri) confid = confid, key = key } - return opts, nil + return opts end -function _M.get(kms_uri) - if not is_kms_uri(kms_uri) then - return nil - end +local function fetch_by_uri(kms_uri) local opts, err = parse_kms_uri(kms_uri) if not opts then - core.log.warn(err) - return nil + return nil, err end + local conf = kms_kv(opts.service, opts.confid) if not conf then - core.log.error("no config") - return nil + return nil, "no kms conf, kms_uri: " .. kms_uri end + local sm = require("apisix.kms." .. opts.service) if not sm then - core.log.error("no kms service: ", opts.service) - return nil + return nil, "no kms service: ", opts.service end + local value, err = sm.get(conf, opts.key) if err then - core.log.error(err) - return nil + 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 1343002fcb20..8ecda6f7fb10 100644 --- a/apisix/kms/vault.lua +++ b/apisix/kms/vault.lua @@ -18,15 +18,33 @@ --- 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 schema_def = require("apisix.schema_def") 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 = schema_def.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 0bdb56ecd0e0..f7b117af93da 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/t/admin/kms.t b/t/admin/kms.t index 8290f77f58e8..feeebbffccb7 100644 --- a/t/admin/kms.t +++ b/t/admin/kms.t @@ -35,6 +35,7 @@ run_tests; __DATA__ === TEST 1: PUT +--- ONLY --- config location /t { content_by_lua_block { diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index 200693a68b06..f21669929c21 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -34,6 +34,19 @@ deployment: _EOC_ $block->set_value("yaml_config", $yaml_config); + + my $routes = <<_EOC_; +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +_EOC_ + + $block->set_value("apisix_yaml", $block->apisix_yaml . $routes); }); run_tests(); @@ -137,7 +150,7 @@ Success! Data written to: kv/apisix/apisix-key -=== TEST 5: kms.get: start with $kms:// +=== TEST 5: kms.fetch_by_uri: start with $kms:// --- apisix_yaml kms: - id: vault/1 @@ -149,7 +162,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/1/apisix-key/key") + local value = kms.fetch_by_uri("$kms://vault/1/apisix-key/key") ngx.say(value) } } @@ -160,7 +173,7 @@ value -=== TEST 6: kms.get: start with $KMS:// +=== TEST 6: kms.fetch_by_uri: start with $KMS:// --- apisix_yaml kms: - id: vault/1 @@ -172,7 +185,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$KMS://vault/1/apisix-key/key") + local value = kms.fetch_by_uri("$KMS://vault/1/apisix-key/key") ngx.say(value) } } @@ -183,7 +196,7 @@ value -=== TEST 7: kms.get, wrong ref format: wrong type +=== TEST 7: kms.fetch_by_uri, wrong ref format: wrong type --- apisix_yaml kms: - id: vault/1 @@ -195,18 +208,18 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get(1) - ngx.say(value) + local _, err = kms.fetch_by_uri(1) + ngx.say(err) } } --- request GET /t --- response_body -nil +error kms_uri type: number -=== TEST 8: kms.get, wrong ref format: wrong prefix +=== TEST 8: kms.fetch_by_uri, wrong ref format: wrong prefix --- apisix_yaml kms: - id: vault/1 @@ -218,18 +231,18 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("kms://") - ngx.say(value) + local _, err = kms.fetch_by_uri("kms://") + ngx.say(err) } } --- request GET /t --- response_body -nil +error kms_uri prefix: kms:// -=== TEST 9: kms.get, error format: no kms service +=== TEST 9: kms.fetch_by_uri, error format: no kms service --- apisix_yaml kms: - id: vault/1 @@ -241,20 +254,18 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://") - ngx.say(value) + local _, err = kms.fetch_by_uri("$kms://") + ngx.say(err) } } --- request GET /t --- response_body -nil ---- error_log error format: no kms service -=== TEST 10: kms.get, error format: no kms conf id +=== TEST 10: kms.fetch_by_uri, error format: no kms conf id --- apisix_yaml kms: - id: vault/1 @@ -266,20 +277,18 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/") - ngx.say(value) + local _, err = kms.fetch_by_uri("$kms://vault/") + ngx.say(err) } } --- request GET /t --- response_body -nil ---- error_log error format: no kms conf id -=== TEST 11: kms.get, error format: no kms key id +=== TEST 11: kms.fetch_by_uri, error format: no kms key id --- apisix_yaml kms: - id: vault/1 @@ -291,20 +300,18 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/2/") - ngx.say(value) + local _, err = kms.fetch_by_uri("$kms://vault/2/") + ngx.say(err) } } --- request GET /t --- response_body -nil ---- error_log error format: no kms key id -=== TEST 12: kms.get, no config +=== TEST 12: kms.fetch_by_uri, no config --- apisix_yaml kms: - id: vault/1 @@ -316,23 +323,21 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://vault/2/bar") - ngx.say(value) + local _, err = kms.fetch_by_uri("$kms://vault/2/bar") + ngx.say(err) } } --- request GET /t --- response_body -nil ---- error_log -no config +no kms conf, kms_uri: $kms://vault/2/bar -=== TEST 13: kms.get, no kms service +=== TEST 13: kms.fetch_by_uri, no sub key value --- apisix_yaml kms: - - id: vault/apisix-key + - id: vault/1 prefix: kv/apisix token: root uri: http://127.0.0.1:8200 @@ -341,7 +346,7 @@ kms: location /t { content_by_lua_block { local kms = require("apisix.kms") - local value = kms.get("$kms://dummy/1/bar") + local value = kms.fetch_by_uri("$kms://vault/1/apisix-key/bar") ngx.say(value) } } @@ -349,25 +354,103 @@ kms: GET /t --- response_body nil ---- error_log -no config -=== TEST 14: kms.get, no sub key value ---- apisix_yaml -kms: - - id: vault/1 - prefix: kv/apisix - token: root - uri: http://127.0.0.1:8200 -#END +=== 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 value = kms.get("$kms://vault/1/apisix-key/bar") - ngx.say(value) + 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 diff --git a/t/core/env.t b/t/core/env.t index 745fcef1aea5..2e14a4397e73 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 979f08e51062..4e6b0d76619d 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, core.env.get) - 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, core.env.get, true, "key", 1) - local refs_2 = core.utils.retrieve_secrets_ref(refs, core.env.get, 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, core.env.get) - 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, core.env.get) - ngx.say(new_refs) - } - } ---- request -GET /t ---- response_body -nil From da39fd0ab871e4a58e57d7d5a6e898ed8010ddee Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:08:16 +0800 Subject: [PATCH 16/23] remove retrieve_secrets_ref --- apisix/consumer.lua | 8 +------ apisix/core/utils.lua | 54 ------------------------------------------- 2 files changed, 1 insertion(+), 61 deletions(-) diff --git a/apisix/consumer.lua b/apisix/consumer.lua index 5e2c3db4733e..bdeb78cd3f8c 100644 --- a/apisix/consumer.lua +++ b/apisix/consumer.lua @@ -98,19 +98,13 @@ function _M.consumers() end -local function retrieve_secrets_callback(key) - return core.env.get(key) or kms.get(key) -end - - local function create_consume_cache(consumers_conf, key_attr) local consumer_names = {} 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, retrieve_secrets_callback) + 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/utils.lua b/apisix/core/utils.lua index c6946efac7c3..98672ded2286 100644 --- a/apisix/core/utils.lua +++ b/apisix/core/utils.lua @@ -331,58 +331,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, callback) - for k, v in pairs(refs) do - local typ = type(v) - if typ == "string" then - refs[k] = callback(v) or v - elseif typ == "table" then - retrieve_ref(v, callback) - end - end - return refs - end - - local function retrieve(refs, callback) - log.info("retrieve secrets refs") - - local new_refs = table.deepcopy(refs) - return retrieve_ref(new_refs, callback) - end - - function retrieve_secrets_ref(refs, callback, cache, key, version) - if not refs or type(refs) ~= "table" then - return nil - end - if not cache then - return retrieve(refs, callback) - end - return secrets_lrucache(key, version, retrieve, refs, callback) - 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 or kms. --- --- @function core.utils.retrieve_secrets_ref --- @tparam table refs The table to be retrieved. --- @tparam function callback The replacement function to use when iterating over values. --- @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 From e25a71b466fab5814540131f3e099a04aa7fad98 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:10:16 +0800 Subject: [PATCH 17/23] remove flag --- t/admin/kms.t | 1 - 1 file changed, 1 deletion(-) diff --git a/t/admin/kms.t b/t/admin/kms.t index feeebbffccb7..8290f77f58e8 100644 --- a/t/admin/kms.t +++ b/t/admin/kms.t @@ -35,7 +35,6 @@ run_tests; __DATA__ === TEST 1: PUT ---- ONLY --- config location /t { content_by_lua_block { From 33d3ed1c1a1a957de9a6782e489b23e103d3bead Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:18:22 +0800 Subject: [PATCH 18/23] fix docs --- apisix/core/utils.lua | 2 -- docs/en/latest/terminology/kms.md | 12 ++++++------ docs/zh/latest/terminology/kms.md | 14 +++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua index 98672ded2286..f72996b78d99 100644 --- a/apisix/core/utils.lua +++ b/apisix/core/utils.lua @@ -25,7 +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 lrucache = require("apisix.core.lrucache") local dns_client = require("apisix.core.dns.client") local ngx_re = require("ngx.re") local ipmatcher = require("resty.ipmatcher") @@ -36,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 diff --git a/docs/en/latest/terminology/kms.md b/docs/en/latest/terminology/kms.md index e2602ccf76b0..7a7fac0a9174 100644 --- a/docs/en/latest/terminology/kms.md +++ b/docs/en/latest/terminology/kms.md @@ -124,20 +124,20 @@ Using Vault to manage secrets means that you can store secrets information in th $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 +- 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: +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 \ @@ -149,7 +149,7 @@ curl http://127.0.0.1:9180/apisix/admin/kms/vault/1 \ }' ``` -If you use APISIX Standalone mode, you can add the following configuration in apisix.yaml: +If you use APISIX Standalone mode, you can add the following configuration in `apisix.yaml` configuration file: ```yaml kms: diff --git a/docs/zh/latest/terminology/kms.md b/docs/zh/latest/terminology/kms.md index e727236c52fd..15404fbf3e76 100644 --- a/docs/zh/latest/terminology/kms.md +++ b/docs/zh/latest/terminology/kms.md @@ -116,7 +116,7 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \ ## 使用 Vault 管理密钥 -使用 Vault 来管理密钥意味着你可以将密钥信息保存在 Vault 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 [Vault KV 引擎的 V1 版本](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1). +使用 Vault 来管理密钥意味着你可以将密钥信息保存在 Vault 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 [Vault KV 引擎的 V1 版本](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1)。 ### 引用方式 @@ -124,20 +124,20 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \ $KMS://$secretmanager/$id/$secret_id/$key ``` -- secretmanager: 密钥管理服务,可以是 vault、aws 等 -- id:KMS 资源 id, 需要和添加 KMS 资源时指定的保持一致 -- secret_id: 密钥管理服务中的密钥 id +- secretmanager: 密钥管理服务,可以是 Vault、AWS 等 +- id:KMS 资源 ID, 需要与添加 KMS 资源时指定的 ID 保持一致 +- secret_id: 密钥管理服务中的密钥 ID - key: 密钥管理服务中密钥对应的 key ### 示例:在 key-auth 插件中使用 第一步:在 Vault 中创建对应的密钥,可以使用如下命令: -```bash +```shell vault kv put apisix/jack auth-key=value ``` -第二步:通过 Admin API 添加 KMS 资源,配置 vault 的地址等连接信息: +第二步:通过 Admin API 添加 KMS 资源,配置 Vault 的地址等连接信息: ```shell curl http://127.0.0.1:9180/apisix/admin/kms/vault/1 \ @@ -149,7 +149,7 @@ curl http://127.0.0.1:9180/apisix/admin/kms/vault/1 \ }' ``` -如果使用 APISIX Standalone 版本,则可以在 apisix.yaml 中添加如下配置: +如果使用 APISIX Standalone 版本,则可以在 `apisix.yaml` 文件中添加如下配置: ```yaml kms: From c61094fba1d046fba09b096b5df53efa66f6374e Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:22:11 +0800 Subject: [PATCH 19/23] fix lint --- apisix/kms.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/apisix/kms.lua b/apisix/kms.lua index 962094e63347..60a598880771 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -25,6 +25,7 @@ local upper = string.upper local byte = string.byte local type = type local pcall = pcall +local pairs = pairs local ipairs = ipairs local error = error From d579f2e291c0ba1e3432155497939e3da7c85918 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 16:30:09 +0800 Subject: [PATCH 20/23] remove redundant code from test cases --- t/config-center-yaml/kms.t | 56 ++++---------------------------------- 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/t/config-center-yaml/kms.t b/t/config-center-yaml/kms.t index f21669929c21..84b2c4c77f89 100644 --- a/t/config-center-yaml/kms.t +++ b/t/config-center-yaml/kms.t @@ -35,7 +35,8 @@ _EOC_ $block->set_value("yaml_config", $yaml_config); - my $routes = <<_EOC_; + if (!$block->apisix_yaml) { + my $routes = <<_EOC_; routes: - uri: /hello @@ -46,7 +47,9 @@ routes: #END _EOC_ - $block->set_value("apisix_yaml", $block->apisix_yaml . $routes); + $block->set_value("apisix_yaml", $routes); + } + }); run_tests(); @@ -136,13 +139,6 @@ uri: http://127.0.0.1:8200 === TEST 4: store secret into vault ---- apisix_yaml -kms: - - id: vault/1 - prefix: kv/apisix - token: root - uri: http://127.0.0.1:8200 -#END --- exec VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/apisix-key key=value --- response_body @@ -197,13 +193,6 @@ value === TEST 7: kms.fetch_by_uri, wrong ref format: wrong type ---- 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 { @@ -220,13 +209,6 @@ error kms_uri type: number === TEST 8: kms.fetch_by_uri, wrong ref format: wrong prefix ---- 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 { @@ -243,13 +225,6 @@ error kms_uri prefix: kms:// === TEST 9: kms.fetch_by_uri, error format: no kms service ---- 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 { @@ -266,13 +241,6 @@ error format: no kms service === TEST 10: kms.fetch_by_uri, error format: no kms conf id ---- 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 { @@ -289,13 +257,6 @@ error format: no kms conf id === TEST 11: kms.fetch_by_uri, error format: no kms key id ---- 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 { @@ -312,13 +273,6 @@ error format: no kms key id === TEST 12: kms.fetch_by_uri, no config ---- 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 { From 7e1b40c250b545cd21c737e9ac6f2d1e53b40490 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 17:54:43 +0800 Subject: [PATCH 21/23] fix comment --- apisix/kms.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index 60a598880771..bdbe14ed029d 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -63,7 +63,7 @@ local function create_kms_kvs(values) local idx = find(path, "/") if not idx then core.log.error("no kms id") - return + return nil end local service = sub(path, 1, idx - 1) @@ -102,17 +102,12 @@ end function _M.init_worker() - local err local cfg = { automatic = true, checker = check_kms, } - kmss, err = core.config.new("/kms", cfg) - if not kmss then - error("failed to create etcd instance for fetching kmss: " .. err) - return - end + kmss = core.config.new("/kms", cfg) end From f31814ccfded55ad6f7cfd3d9388e976bb6f389d Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 18:07:19 +0800 Subject: [PATCH 22/23] fix lint --- apisix/kms.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index bdbe14ed029d..d9b8f0c39e46 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -27,7 +27,6 @@ local type = type local pcall = pcall local pairs = pairs local ipairs = ipairs -local error = error local _M = {} From ce4a817dc00f985b2fe4c831bb2f6f4b061d7bd3 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 30 Nov 2022 19:11:46 +0800 Subject: [PATCH 23/23] use core.schema.uri_def --- apisix/kms.lua | 6 +++--- apisix/kms/vault.lua | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apisix/kms.lua b/apisix/kms.lua index d9b8f0c39e46..2da2348636f0 100644 --- a/apisix/kms.lua +++ b/apisix/kms.lua @@ -158,9 +158,9 @@ local function fetch_by_uri(kms_uri) return nil, "no kms conf, kms_uri: " .. kms_uri end - local sm = require("apisix.kms." .. opts.service) - if not sm then - return nil, "no kms service: ", opts.service + 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) diff --git a/apisix/kms/vault.lua b/apisix/kms/vault.lua index 8ecda6f7fb10..41111e2b7f22 100644 --- a/apisix/kms/vault.lua +++ b/apisix/kms/vault.lua @@ -20,7 +20,6 @@ local core = require("apisix.core") local http = require("resty.http") -local schema_def = require("apisix.schema_def") local norm_path = require("pl.path").normpath @@ -31,7 +30,7 @@ local rfind_char = core.string.rfind_char local schema = { type = "object", properties = { - uri = schema_def.uri_def, + uri = core.schema.uri_def, prefix = { type = "string", },