From b2788ffb0d3c52bc460d6500c4318ad18312860c Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Sat, 4 Dec 2021 00:21:57 +0530 Subject: [PATCH 01/25] vault-auth init --- apisix/plugins/vault-auth.lua | 144 +++++++++++++++++++++++++++++++ apisix/plugins/vault/request.lua | 49 +++++++++++ conf/config-default.yaml | 1 + t/admin/plugins.t | 3 +- 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 apisix/plugins/vault-auth.lua create mode 100644 apisix/plugins/vault/request.lua diff --git a/apisix/plugins/vault-auth.lua b/apisix/plugins/vault-auth.lua new file mode 100644 index 000000000000..83d6b4e65bed --- /dev/null +++ b/apisix/plugins/vault-auth.lua @@ -0,0 +1,144 @@ +-- +-- 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 core = require("apisix.core") +local consumer_mod = require("apisix.consumer") +local resty_random = require("resty.random") +local hex_encode = require("resty.string").to_hex +-- local vault_fetch = require("apisix.plugins.vault.request").fetch + +local ipairs = ipairs +local plugin_name = "vault-auth" + + +local lrucache = core.lrucache.new({ + type = "plugin", +}) + +local schema = { + type = "object", + properties = {}, +} + +local consumer_schema = { + type = "object", + properties = { + accesskey = {type = "string"}, + secretkey = {type = "string"} + } +} + +local metadata_schema = { + type = "object", + properties = { + host = {type = "string", default = "127.0.0.1"}, + port = {type = "integer", default = 8200}, + vault_token = {type = "string"}, + vault_kv_path = {type = "string", default = "kv/apisix/plugins/vault-auth"} + } +} + +local _M = { + version = 0.1, + priority = 2535, + type = 'auth', + name = plugin_name, + schema = schema, + consumer_schema = consumer_schema, + metadata_schema = metadata_schema, +} + + +local create_consume_cache +do + local consumer_names = {} + + function create_consume_cache(consumers) + core.table.clear(consumer_names) + + for _, consumer in ipairs(consumers.nodes) do + core.log.info("consumer node: ", core.json.delay_encode(consumer)) + consumer_names[consumer.auth_conf.accesskey] = consumer + end + + return consumer_names + end + +end -- do + + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_CONSUMER then + local ok, err = core.schema.check(consumer_schema, conf) + if not ok then + return false, err + end + + elseif schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + else + return core.schema.check(schema, conf) + end + + if not conf.accesskey then + conf.accesskey = hex_encode(resty_random.bytes(16, true)) + end + if not conf.secretkey then + conf.secretkey = hex_encode(resty_random.bytes(16, true)) + end + + return true +end + + +function _M.rewrite(conf, ctx) + local uri_args = core.request.get_uri_args(ctx) or {} + local headers = core.request.headers(ctx) or {} + + local accesskey = headers.accesskey or uri_args.accesskey + local secretkey = headers.secretkey or uri_args.secretkey + + if not accesskey then + return 401, {message = "Missing accesskey and secretkey found in request"} + end + + local consumer_conf = consumer_mod.plugin(plugin_name) + if not consumer_conf then + return 401, {message = "Missing related consumer"} + end + + local consumers = lrucache("consumers_key", consumer_conf.conf_version, + create_consume_cache, consumer_conf) + + local consumer = consumers[accesskey] + if not consumer then + return 401, {message = "Invalid accesskey attached with the request"} + end + + core.log.error(core.json.delay_encode(consumer, true)) + core.log.error("sec ", secretkey) + + if consumer.auth_conf.secretkey ~= secretkey then + return 401, {message = "Invalid secretkey attached with the request"} + end + core.log.info("consumer: ", core.json.delay_encode(consumer)) + + consumer_mod.attach_consumer(ctx, consumer, consumer_conf) + core.log.info("hit vault-auth rewrite") +end + + +return _M diff --git a/apisix/plugins/vault/request.lua b/apisix/plugins/vault/request.lua new file mode 100644 index 000000000000..9b92e820bbab --- /dev/null +++ b/apisix/plugins/vault/request.lua @@ -0,0 +1,49 @@ +-- +-- 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 http = require("resty.http") +local core = require("apisix.core") +local json = require("cjson") + +local _M = {} + +local function _vault_fetch (host_addr, path, method, vault_token) + local httpc = http.new() + local res, err = httpc:request_uri(host_addr, { + method = method, + path = path, + headers = { + ["X-Vault-Token"] = vault_token + } + }) + + if not res or err then + core.log.error("failed to fetch data from vault server running on: ", host_addr + " with error: ", err) + return {} + end + + local tab = json.decode(res.body) + if not tab then + return {} + end + + return tab.data +end +_M.fetch = _vault_fetch + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 7cf130b256cb..ebba00e90b4d 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -318,6 +318,7 @@ plugins: # plugin list (sorted by priority) - authz-casbin # priority: 2560 - wolf-rbac # priority: 2555 - ldap-auth # priority: 2540 + - vault-auth # priority: 2535 - hmac-auth # priority: 2530 - basic-auth # priority: 2520 - jwt-auth # priority: 2510 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index dbe585997b08..ffa360229184 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -78,6 +78,7 @@ openid-connect authz-casbin wolf-rbac ldap-auth +vault-auth hmac-auth basic-auth jwt-auth @@ -306,7 +307,7 @@ qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\}," } } --- response_body eval -qr/\[\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ +qr/\[\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"vault-auth","priority":2535\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ --- no_error_log [error] From a88c6155357d66fa397d91c47271e99f110224ac Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Mon, 6 Dec 2021 15:52:20 +0530 Subject: [PATCH 02/25] vault storage kv engine integration --- apisix/admin/consumers.lua | 51 ++++++++++++++++ apisix/core/vault.lua | 111 ++++++++++++++++++++++++++++++++++ apisix/plugins/vault-auth.lua | 35 +++++------ conf/config-default.yaml | 7 +++ 4 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 apisix/core/vault.lua diff --git a/apisix/admin/consumers.lua b/apisix/admin/consumers.lua index 46b23de09bdb..684d478e9b2e 100644 --- a/apisix/admin/consumers.lua +++ b/apisix/admin/consumers.lua @@ -18,6 +18,9 @@ local core = require("apisix.core") local plugins = require("apisix.admin.plugins") local utils = require("apisix.admin.utils") local plugin = require("apisix.plugin") +local vault = require("apisix.core.vault") + +local tab_clone = require("apisix.core.table").clone local pairs = pairs local _M = { @@ -74,6 +77,27 @@ function _M.put(username, conf) local key = "/consumers/" .. consumer_name core.log.info("key: ", key) + -- vault-auth details gets stored into vault for security concern + local vault_restore + if conf.plugins and conf.plugins["vault-auth"] then + vault_restore = tab_clone(conf.plugins["vault-auth"]) + + conf.plugins["vault-auth"].secretkey = nil + conf.plugins["vault-auth"]["fetch-vault"] = true + + -- store into vault with the path set to plugin accesskey + local vkey = "/consumers/auth-data/" .. vault_restore.accesskey + + local vres, err = vault.set(vkey, vault_restore) + if not vres or err then + core.log.error("failed to store data for suffix path[", vkey, + "]: into vault server, error: ", err) + return 503, {error = err} + end + + end + core.log.error(core.json.delay_encode(conf, true)) + local ok, err = utils.inject_conf_with_prev_conf("consumer", key, conf) if not ok then return 503, {error_msg = err} @@ -85,6 +109,13 @@ function _M.put(username, conf) return 503, {error_msg = err} end + -- modify the body, some data were not stored into etcd + if vault_restore then + res.body.vault = { + ["data-stored"] = vault_restore + } + end + return res.status, res.body end @@ -102,6 +133,26 @@ function _M.get(consumer_name) end utils.fix_count(res.body, consumer_name) + + if consumer_name then + local _plugins = res.body.node.value.plugins + + if _plugins and _plugins["vault-auth"] then + local vkey = "/consumers/auth-data/" .. _plugins["vault-auth"].accesskey + + local vres, err = vault.get(vkey) + + if not vres then + core.log.error("failed to fetch data for suffix path[", vkey, + "]: into vault server, error: ", err) + return 503, {error_msg = err} + end + + res.body.vault = { + ["data-fetched"] = vres + } + end + end return res.status, res.body end diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua new file mode 100644 index 000000000000..53c229a83137 --- /dev/null +++ b/apisix/core/vault.lua @@ -0,0 +1,111 @@ +-- +-- 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 core = require("apisix.core") +local http = require("resty.http") +local json = require("cjson") + +local fetch_local_conf = require("apisix.core.config_local").local_conf +local norm_path = require("pl.path").normpath + +local _M = {} + +local function fetch_vault_conf() + local conf, err = fetch_local_conf() + if not conf then + return nil, "failed to fetch vault configuration from config yaml: " .. err + end + + if not conf.vault then + return nil, "accessing vault data requires configuration information" + end + return conf.vault +end + + +local function make_request_to_vault(method, key, data) + local vault, err = fetch_vault_conf() + if not vault then + return nil, err + end + + local httpc = http.new() + -- config timeout or default to 5000 ms + httpc:set_timeout((vault.timeout or 5)*1000) + + local req_addr = vault.host .. norm_path("/v1/" + .. vault.prefix .. "/" .. key) + + local res, err = httpc:request_uri(req_addr, { + method = method, + headers = { + ["X-Vault-Token"] = vault.token + }, + body = core.json.encode(data or {}, true) + }) + if not res then + return nil, err + end + + return res.body +end + +-- key is the vault kv engine path, joined with config yaml vault prefix +local function get(key) + core.log.info("fetching data from vault for key: ", key) + + local res, err = make_request_to_vault("GET", key) + if not res or err then + return nil, "failed to retrtive data from vault kv engine " .. err + end + + return json.decode(res) +end + +_M.get = get + +-- key is the vault kv engine path, data is json key vaule pair +local function set(key, data) + core.log.info("stroing data into vault for key: ", key, + "and value: ", core.json.delay_encode(data, true)) + + local res, err = make_request_to_vault("POST", key, data) + if not res or err then + return nil, "failed to store data into vault kv engine " .. err + end + + return {status = "success"} +end +_M.set = set + + +-- key is the vault kv engine path, joined with config yaml vault prefix +local function delete(key) + core.log.info("deleting data from vault for key: ", key) + + local res, err = make_request_to_vault("DELETE", key) + + if not res or err then + return nil, "failed to delete data into vault kv engine " .. err + end + + return {status = "success"} +end + +_M.delete = delete + +return _M diff --git a/apisix/plugins/vault-auth.lua b/apisix/plugins/vault-auth.lua index 83d6b4e65bed..51cd2099de7e 100644 --- a/apisix/plugins/vault-auth.lua +++ b/apisix/plugins/vault-auth.lua @@ -17,8 +17,9 @@ local core = require("apisix.core") local consumer_mod = require("apisix.consumer") local resty_random = require("resty.random") +local vault = require("apisix.core.vault") + local hex_encode = require("resty.string").to_hex --- local vault_fetch = require("apisix.plugins.vault.request").fetch local ipairs = ipairs local plugin_name = "vault-auth" @@ -41,15 +42,6 @@ local consumer_schema = { } } -local metadata_schema = { - type = "object", - properties = { - host = {type = "string", default = "127.0.0.1"}, - port = {type = "integer", default = 8200}, - vault_token = {type = "string"}, - vault_kv_path = {type = "string", default = "kv/apisix/plugins/vault-auth"} - } -} local _M = { version = 0.1, @@ -58,7 +50,6 @@ local _M = { name = plugin_name, schema = schema, consumer_schema = consumer_schema, - metadata_schema = metadata_schema, } @@ -70,7 +61,7 @@ do core.table.clear(consumer_names) for _, consumer in ipairs(consumers.nodes) do - core.log.info("consumer node: ", core.json.delay_encode(consumer)) + core.log.error("consumer node: ", core.json.delay_encode(consumer, true)) consumer_names[consumer.auth_conf.accesskey] = consumer end @@ -86,9 +77,6 @@ function _M.check_schema(conf, schema_type) if not ok then return false, err end - - elseif schema_type == core.schema.TYPE_METADATA then - return core.schema.check(metadata_schema, conf) else return core.schema.check(schema, conf) end @@ -123,20 +111,29 @@ function _M.rewrite(conf, ctx) local consumers = lrucache("consumers_key", consumer_conf.conf_version, create_consume_cache, consumer_conf) + -- check local cache to verify if the accesskey is there (the etcd doesn't contains + -- the password). Actually saves a roundtrip to vault when accesskey itself is invalid. local consumer = consumers[accesskey] + core.log.error(core.json.delay_encode(consumer, true)) -- see this log @spacewander + if not consumer then return 401, {message = "Invalid accesskey attached with the request"} end - core.log.error(core.json.delay_encode(consumer, true)) - core.log.error("sec ", secretkey) + -- fetching the secretkey from vault and perform matching. + local res, err = vault.get("/consumers/auth-data/" .. accesskey) + if not res or err then + core.log.error("failed to get secret key for access key[ ", accesskey, + " ] from vault: ", err) + return 503, {message = "Issue with authenticating with vault server"} + end - if consumer.auth_conf.secretkey ~= secretkey then + if res.data.secretkey ~= secretkey then return 401, {message = "Invalid secretkey attached with the request"} end - core.log.info("consumer: ", core.json.delay_encode(consumer)) consumer_mod.attach_consumer(ctx, consumer, consumer_conf) + core.log.info("hit vault-auth rewrite") end diff --git a/conf/config-default.yaml b/conf/config-default.yaml index ebba00e90b4d..55667be4eb3d 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -278,6 +278,13 @@ etcd: # the default value is true, e.g. the certificate will be verified strictly. #sni: # the SNI for etcd TLS requests. If missed, the host part of the URL will be used. +# storage backend for sensitive data storage and retrieval +vault: + host: "http://127.0.0.1:8200" + timeout: 10 + prefix: kv/apisix + token: s.0L81KEDHYZnjLMFleFIfXDWV + #discovery: # service discovery center # dns: # servers: From 17b3c236ae7043a56c515c4b143c23d3abf22347 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Mon, 6 Dec 2021 16:05:59 +0530 Subject: [PATCH 03/25] not required file --- apisix/plugins/vault/request.lua | 49 -------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 apisix/plugins/vault/request.lua diff --git a/apisix/plugins/vault/request.lua b/apisix/plugins/vault/request.lua deleted file mode 100644 index 9b92e820bbab..000000000000 --- a/apisix/plugins/vault/request.lua +++ /dev/null @@ -1,49 +0,0 @@ --- --- 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 http = require("resty.http") -local core = require("apisix.core") -local json = require("cjson") - -local _M = {} - -local function _vault_fetch (host_addr, path, method, vault_token) - local httpc = http.new() - local res, err = httpc:request_uri(host_addr, { - method = method, - path = path, - headers = { - ["X-Vault-Token"] = vault_token - } - }) - - if not res or err then - core.log.error("failed to fetch data from vault server running on: ", host_addr - " with error: ", err) - return {} - end - - local tab = json.decode(res.body) - if not tab then - return {} - end - - return tab.data -end -_M.fetch = _vault_fetch - -return _M From 7605c382a3246fffdf8edddc2b4077a5c4defa93 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Wed, 8 Dec 2021 19:05:54 +0530 Subject: [PATCH 04/25] integrating vault storage backend with jwt-auth authentication plugin --- apisix/admin/consumers.lua | 61 ++++------- apisix/core/vault.lua | 21 ++-- apisix/plugins/jwt-auth.lua | 184 +++++++++++++++++++++++++++++----- apisix/plugins/vault-auth.lua | 141 -------------------------- conf/config-default.yaml | 13 ++- t/admin/plugins.t | 3 +- 6 files changed, 201 insertions(+), 222 deletions(-) delete mode 100644 apisix/plugins/vault-auth.lua diff --git a/apisix/admin/consumers.lua b/apisix/admin/consumers.lua index 684d478e9b2e..b5bd3e2da61f 100644 --- a/apisix/admin/consumers.lua +++ b/apisix/admin/consumers.lua @@ -19,8 +19,6 @@ local plugins = require("apisix.admin.plugins") local utils = require("apisix.admin.utils") local plugin = require("apisix.plugin") local vault = require("apisix.core.vault") - -local tab_clone = require("apisix.core.table").clone local pairs = pairs local _M = { @@ -77,27 +75,6 @@ function _M.put(username, conf) local key = "/consumers/" .. consumer_name core.log.info("key: ", key) - -- vault-auth details gets stored into vault for security concern - local vault_restore - if conf.plugins and conf.plugins["vault-auth"] then - vault_restore = tab_clone(conf.plugins["vault-auth"]) - - conf.plugins["vault-auth"].secretkey = nil - conf.plugins["vault-auth"]["fetch-vault"] = true - - -- store into vault with the path set to plugin accesskey - local vkey = "/consumers/auth-data/" .. vault_restore.accesskey - - local vres, err = vault.set(vkey, vault_restore) - if not vres or err then - core.log.error("failed to store data for suffix path[", vkey, - "]: into vault server, error: ", err) - return 503, {error = err} - end - - end - core.log.error(core.json.delay_encode(conf, true)) - local ok, err = utils.inject_conf_with_prev_conf("consumer", key, conf) if not ok then return 503, {error_msg = err} @@ -109,13 +86,6 @@ function _M.put(username, conf) return 503, {error_msg = err} end - -- modify the body, some data were not stored into etcd - if vault_restore then - res.body.vault = { - ["data-stored"] = vault_restore - } - end - return res.status, res.body end @@ -135,24 +105,31 @@ function _M.get(consumer_name) utils.fix_count(res.body, consumer_name) if consumer_name then - local _plugins = res.body.node.value.plugins - - if _plugins and _plugins["vault-auth"] then - local vkey = "/consumers/auth-data/" .. _plugins["vault-auth"].accesskey - - local vres, err = vault.get(vkey) - - if not vres then - core.log.error("failed to fetch data for suffix path[", vkey, - "]: into vault server, error: ", err) - return 503, {error_msg = err} + -- if data is queried for a single consumer, and there is any plugin where the vault config + -- is enabled - it fetches vault data and returns combined with etcd response. + local vault_fetch = {} + local attach_response = false + local _plugins = res.body.node.value.plugins or {} + for plugin_name, _schema in pairs(_plugins) do + if _schema.vault then + local res, err = vault.get(_schema.vault.path, _schema.vault.add_prefix) + if not res then + core.log.error("failed to get data from vault for plugin: ", plugin_name, + "err: ", err) + else + attach_response = true + vault_fetch[plugin_name] = res.data + end end + end + if attach_response then res.body.vault = { - ["data-fetched"] = vres + ["data-fetched"] = vault_fetch } end end + return res.status, res.body end diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua index 53c229a83137..813882da2762 100644 --- a/apisix/core/vault.lua +++ b/apisix/core/vault.lua @@ -37,7 +37,7 @@ local function fetch_vault_conf() end -local function make_request_to_vault(method, key, data) +local function make_request_to_vault(method, key, rel_path, data) local vault, err = fetch_vault_conf() if not vault then return nil, err @@ -47,8 +47,13 @@ local function make_request_to_vault(method, key, data) -- config timeout or default to 5000 ms httpc:set_timeout((vault.timeout or 5)*1000) - local req_addr = vault.host .. norm_path("/v1/" + local req_addr = vault.host + if rel_path then + req_addr = req_addr .. norm_path("/v1/" .. vault.prefix .. "/" .. key) + else + req_addr = req_addr .. norm_path("/" .. key) + end local res, err = httpc:request_uri(req_addr, { method = method, @@ -65,10 +70,10 @@ local function make_request_to_vault(method, key, data) end -- key is the vault kv engine path, joined with config yaml vault prefix -local function get(key) +local function get(key, rel_path) core.log.info("fetching data from vault for key: ", key) - local res, err = make_request_to_vault("GET", key) + local res, err = make_request_to_vault("GET", key, rel_path) if not res or err then return nil, "failed to retrtive data from vault kv engine " .. err end @@ -79,11 +84,11 @@ end _M.get = get -- key is the vault kv engine path, data is json key vaule pair -local function set(key, data) +local function set(key, data, rel_path) core.log.info("stroing data into vault for key: ", key, "and value: ", core.json.delay_encode(data, true)) - local res, err = make_request_to_vault("POST", key, data) + local res, err = make_request_to_vault("POST", key, rel_path, data) if not res or err then return nil, "failed to store data into vault kv engine " .. err end @@ -94,10 +99,10 @@ _M.set = set -- key is the vault kv engine path, joined with config yaml vault prefix -local function delete(key) +local function delete(key, rel_path) core.log.info("deleting data from vault for key: ", key) - local res, err = make_request_to_vault("DELETE", key) + local res, err = make_request_to_vault("DELETE", key, rel_path) if not res or err then return nil, "failed to delete data into vault kv engine " .. err diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index cf3152a2a1a7..5504b329d8cc 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -19,6 +19,7 @@ local jwt = require("resty.jwt") local ck = require("resty.cookie") local consumer_mod = require("apisix.consumer") local resty_random = require("resty.random") +local vault = require("apisix.core.vault") local ngx_encode_base64 = ngx.encode_base64 local ngx_decode_base64 = ngx.decode_base64 @@ -54,6 +55,13 @@ local consumer_schema = { base64_secret = { type = "boolean", default = false + }, + vault = { + type = "object", + properties = { + path = {type = "string"}, + add_prefix = {type = "boolean"} + } } }, dependencies = { @@ -76,7 +84,23 @@ local consumer_schema = { }, }, required = {"public_key", "private_key"}, - } + }, + { + properties = { + vault = { + type = "object", + properties = { + path = {type = "string"}, + add_prefix = {type = "boolean"} + } + }, + algorithm = { + enum = {"RS256"}, + }, + }, + required = {"vault"}, + }, + } } }, @@ -119,29 +143,74 @@ function _M.check_schema(conf, schema_type) if schema_type == core.schema.TYPE_CONSUMER then ok, err = core.schema.check(consumer_schema, conf) else - ok, err = core.schema.check(schema, conf) + return core.schema.check(schema, conf) end if not ok then return false, err end - if schema_type == core.schema.TYPE_CONSUMER then - if conf.algorithm ~= "RS256" and not conf.secret then - conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) + -- in nginx init_worker_by_lua context API calls are disabled, + -- also that is a costly operation during system startup. + if ngx.get_phase() == "init_worker" then + return true + end + + local vout = {} + if conf.vault then + -- create vault path, if not set by admin. + if not conf.vault.path then + conf.vault.path = "jwt-auth/key/" .. conf.key + conf.vault.add_prefix = true + end + + -- fetch the data to check if the keys are stored into vault + local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + if not res or err then + core.log.error("failed to fetch data from vault: ", err) + return false, "error while fetching data from vault, " .. + "please check the connection or remove vault config" + end + -- if there is no data on that path, that's absolutely fine. + vout = res.data or {} + end + + if conf.algorithm ~= "RS256" then + local secret = conf.secret or vout.secret + -- if no secret is provided, generate one. + if not secret then + secret = ngx_encode_base64(resty_random.bytes(32, true)) + + -- if vault config is enabled, lifecycle of the + -- HS256/HS512 secret will be externally managed by vault. + if conf.vault then + local res, err = vault.set(conf.vault.path, { + secret = secret, + }, conf.vault.add_prefix) + if not res or err then + core.log.error("failed to put data into vault: ", err) + return false, "error communicating with vault server" + end + conf.secret = "" + else + conf.secret = secret + end + elseif conf.base64_secret then - if ngx_decode_base64(conf.secret) == nil then + if ngx_decode_base64(secret) == nil then return false, "base64_secret required but the secret is not in base64 format" end end + end - if conf.algorithm == "RS256" then - if not conf.public_key then - return false, "missing valid public key" - end - if not conf.private_key then - return false, "missing valid private key" - end + if conf.algorithm == "RS256" then + -- check from consumer config and vault data store. Possible options are + -- a) both are in vault, b) both in schema, c) one in schema, another in vault. + if not conf.public_key and not vout.public_key then + return false, "missing valid public key" + end + if not conf.private_key and not vout.private_key then + return false, "missing valid private key" end end @@ -176,11 +245,56 @@ end local function get_secret(conf) + local secret = conf.secret + if conf.vault then + local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + if not res or err then + return nil, err + end + + if not res.data and not res.data.secret then + return nil, "secret could not found in vault: " .. core.json.encode(res) + end + secret = res.data.secret + end + if conf.base64_secret then - return ngx_decode_base64(conf.secret) + return ngx_decode_base64(secret) + end + + return secret +end + + +local function get_rsa_keypair(conf) + local public_key = conf.public_key + local private_key = conf.private_key + -- if keys are present in conf, no need to query vault (fallback) + if public_key and private_key then + return public_key, private_key + end + + local vout = {} + if conf.vault then + local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + if not res or err then + return nil, nil, err + end + + if not res.data then + return nil, nil, "keypairs could not found in vault: " .. core.json.encode(res) + end + vout = res.data end - return conf.secret + if not public_key and not vout.public_key then + return nil, nil, "missing public key, not found in config/vault" + end + if not private_key and not vout.private_key then + return nil, nil, "missing private key, not found in config/vault" + end + + return public_key or vout.public_key, private_key or vout.private_key end @@ -198,7 +312,11 @@ end local function sign_jwt_with_HS(key, auth_conf, payload) - local auth_secret = get_secret(auth_conf) + local auth_secret, err = get_secret(auth_conf) + if not auth_secret then + core.log.error("failed to sign jwt, err: ", err) + core.response.exit(500, "failed to sign jwt") + end local ok, jwt_token = pcall(jwt.sign, _M, auth_secret, { @@ -218,14 +336,20 @@ end local function sign_jwt_with_RS256(key, auth_conf, payload) + local public_key, private_key, err = get_rsa_keypair(auth_conf) + if not public_key then + core.log.error("failed to sign jwt, err: ", err) + core.response.exit(500, "failed to sign jwt") + end + local ok, jwt_token = pcall(jwt.sign, _M, - auth_conf.private_key, + private_key, { header = { typ = "JWT", alg = auth_conf.algorithm, x5c = { - auth_conf.public_key, + public_key, } }, payload = get_real_payload(key, auth_conf, payload) @@ -238,13 +362,22 @@ local function sign_jwt_with_RS256(key, auth_conf, payload) return jwt_token end - -local function algorithm_handler(consumer) +-- introducing method_only flag (returns respective signing method) to save http API calls. +local function algorithm_handler(consumer, method_only) if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm == "HS256" or consumer.auth_conf.algorithm == "HS512" then - return sign_jwt_with_HS, get_secret(consumer.auth_conf) + if method_only then + return sign_jwt_with_HS + end + + return get_secret(consumer.auth_conf) elseif consumer.auth_conf.algorithm == "RS256" then - return sign_jwt_with_RS256, consumer.auth_conf.public_key + if method_only then + return sign_jwt_with_RS256 + end + + local public_key, _, err = get_rsa_keypair(consumer.auth_conf) + return public_key, err end end @@ -284,7 +417,10 @@ function _M.rewrite(conf, ctx) end core.log.info("consumer: ", core.json.delay_encode(consumer)) - local _, auth_secret = algorithm_handler(consumer) + local auth_secret, err = algorithm_handler(consumer) + if not auth_secret then + core.log.error("failed to retrive secrets, err: ", err) + end jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj) core.log.info("jwt object: ", core.json.delay_encode(jwt_obj)) @@ -325,7 +461,7 @@ local function gen_token() core.log.info("consumer: ", core.json.delay_encode(consumer)) - local sign_handler, _ = algorithm_handler(consumer) + local sign_handler = algorithm_handler(consumer, true) local jwt_token = sign_handler(key, consumer.auth_conf, payload) if jwt_token then return core.response.exit(200, jwt_token) diff --git a/apisix/plugins/vault-auth.lua b/apisix/plugins/vault-auth.lua deleted file mode 100644 index 51cd2099de7e..000000000000 --- a/apisix/plugins/vault-auth.lua +++ /dev/null @@ -1,141 +0,0 @@ --- --- 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 core = require("apisix.core") -local consumer_mod = require("apisix.consumer") -local resty_random = require("resty.random") -local vault = require("apisix.core.vault") - -local hex_encode = require("resty.string").to_hex - -local ipairs = ipairs -local plugin_name = "vault-auth" - - -local lrucache = core.lrucache.new({ - type = "plugin", -}) - -local schema = { - type = "object", - properties = {}, -} - -local consumer_schema = { - type = "object", - properties = { - accesskey = {type = "string"}, - secretkey = {type = "string"} - } -} - - -local _M = { - version = 0.1, - priority = 2535, - type = 'auth', - name = plugin_name, - schema = schema, - consumer_schema = consumer_schema, -} - - -local create_consume_cache -do - local consumer_names = {} - - function create_consume_cache(consumers) - core.table.clear(consumer_names) - - for _, consumer in ipairs(consumers.nodes) do - core.log.error("consumer node: ", core.json.delay_encode(consumer, true)) - consumer_names[consumer.auth_conf.accesskey] = consumer - end - - return consumer_names - end - -end -- do - - -function _M.check_schema(conf, schema_type) - if schema_type == core.schema.TYPE_CONSUMER then - local ok, err = core.schema.check(consumer_schema, conf) - if not ok then - return false, err - end - else - return core.schema.check(schema, conf) - end - - if not conf.accesskey then - conf.accesskey = hex_encode(resty_random.bytes(16, true)) - end - if not conf.secretkey then - conf.secretkey = hex_encode(resty_random.bytes(16, true)) - end - - return true -end - - -function _M.rewrite(conf, ctx) - local uri_args = core.request.get_uri_args(ctx) or {} - local headers = core.request.headers(ctx) or {} - - local accesskey = headers.accesskey or uri_args.accesskey - local secretkey = headers.secretkey or uri_args.secretkey - - if not accesskey then - return 401, {message = "Missing accesskey and secretkey found in request"} - end - - local consumer_conf = consumer_mod.plugin(plugin_name) - if not consumer_conf then - return 401, {message = "Missing related consumer"} - end - - local consumers = lrucache("consumers_key", consumer_conf.conf_version, - create_consume_cache, consumer_conf) - - -- check local cache to verify if the accesskey is there (the etcd doesn't contains - -- the password). Actually saves a roundtrip to vault when accesskey itself is invalid. - local consumer = consumers[accesskey] - core.log.error(core.json.delay_encode(consumer, true)) -- see this log @spacewander - - if not consumer then - return 401, {message = "Invalid accesskey attached with the request"} - end - - -- fetching the secretkey from vault and perform matching. - local res, err = vault.get("/consumers/auth-data/" .. accesskey) - if not res or err then - core.log.error("failed to get secret key for access key[ ", accesskey, - " ] from vault: ", err) - return 503, {message = "Issue with authenticating with vault server"} - end - - if res.data.secretkey ~= secretkey then - return 401, {message = "Invalid secretkey attached with the request"} - end - - consumer_mod.attach_consumer(ctx, consumer, consumer_conf) - - core.log.info("hit vault-auth rewrite") -end - - -return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 55667be4eb3d..4ad743c7a4a6 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -280,10 +280,14 @@ etcd: # storage backend for sensitive data storage and retrieval vault: - host: "http://127.0.0.1:8200" - timeout: 10 - prefix: kv/apisix - token: s.0L81KEDHYZnjLMFleFIfXDWV + host: "http://127.0.0.1:8200" # The host address where the vault server is running. + timeout: 10 # request timeout 30 seconds + prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored + # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforce + # policies, generate limited scoped tokens and tightly control the data that can be accessed + # from APISIX. + + token: root # Authentication token to access Vault HTTP APIs #discovery: # service discovery center # dns: @@ -325,7 +329,6 @@ plugins: # plugin list (sorted by priority) - authz-casbin # priority: 2560 - wolf-rbac # priority: 2555 - ldap-auth # priority: 2540 - - vault-auth # priority: 2535 - hmac-auth # priority: 2530 - basic-auth # priority: 2520 - jwt-auth # priority: 2510 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index ffa360229184..dbe585997b08 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -78,7 +78,6 @@ openid-connect authz-casbin wolf-rbac ldap-auth -vault-auth hmac-auth basic-auth jwt-auth @@ -307,7 +306,7 @@ qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\}," } } --- response_body eval -qr/\[\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"vault-auth","priority":2535\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ +qr/\[\{"name":"wolf-rbac","priority":2555\},\{"name":"ldap-auth","priority":2540\},\{"name":"hmac-auth","priority":2530\},\{"name":"basic-auth","priority":2520\},\{"name":"jwt-auth","priority":2510\},\{"name":"key-auth","priority":2500\}\]/ --- no_error_log [error] From c3a7d4af8850e760b6ae9936963f215a337ee78c Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 13:45:56 +0530 Subject: [PATCH 05/25] openssl rsa-2048 pem public private keypairs --- t/certs/private.pem | 27 +++++++++++++++++++++++++++ t/certs/public.pem | 9 +++++++++ 2 files changed, 36 insertions(+) create mode 100644 t/certs/private.pem create mode 100644 t/certs/public.pem diff --git a/t/certs/private.pem b/t/certs/private.pem new file mode 100644 index 000000000000..76f0875f9540 --- /dev/null +++ b/t/certs/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA79XYBopfnVMKxI533oU2VFQbEdSPtWRD+xSl73lHLVboGP1l +SIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7JBUXyl6pysBPfrqC8n/MOXKaD4e8U +5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4Clrd7shAyitB7use6DHcVCKuI4bFO +oFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHAM+47r1iv3lY3ex0P45PRd7U7rq8P +8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1+7njrVQoWvuOTSsc9TDMhZkmmSsU +3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbcBQIDAQABAoIBADHXy1FwqHZVr8Mx +qI/CN4xG/mkyN7uG3unrXKDsH3K4wPuQjeAIr/bu43EOqYl3eLI3sDrpKjsUSCqe +rE1QhE5oPwZuEe+t8aqlFQ5YwP9YS8hEm57qpg5hkBWTBWfxQWVwclilV13JT5W0 +NgpfQwJ3l2lmHFrlARHMOEom5WQrewKvLh2YXeJBFQc0shHcjC2Pt7cjR9oAUVi6 +M5h6I+eB5xd9jj2a2fXaFL1SKZXEBVT6agSQqdB0tSuVTUsTBzNnuTL5ngS1wdLa +lEdrw8klOYWrUihKJgYH7rnQrVEVNxGyO6fVs1S9CxMwu/nW2MPcbRBY0WKYCcAO +QFJ4j4ECgYEA+yaEEPp/SH1E+DJi3U35pGdlHqg8yP0R7sik2cvvPUk4VbPrYVDD +NQ8gt2H+06keycfRqJTPptS79db9LpKjG59yYP3aWj2YbGsH1H3XxA3sZiWHkNl0 +7i0ZE0GSCmEMbPe3C0Z3726tD9ZyVdaE5RdvRWdz1IloA+rYr3ypnH0CgYEA9Hdl +KY8qSthtgWsTuthpExcvfppS3Dijgd23+oZJY2JLKf8/yctuBv6rBgqDCwpnUmGR +tnkxPD/igaBnFtaMjDKNMwWwGHyarWkI7Zc+6HUdNcA/BkI3MCxwYQg2fr7HXY0h +FalewOHeJz2Tldaue9DrVIO49jfLtBh2DYZFvCkCgYBV7OmGPY3KqUEtgV+dw43D +l7Ra9shFI4A9J9xuv30MhL6HY9UGKHGA97oDw71BgT0NYBX1DWS1+VaNV46rnnO7 +gaPKV0+bTDOX9E5rftqRMwpMME7fWebNjhRkKCzk7CsqJN41N1jVTBJdtsrLX2d8 +UbY6EpjogFJb9L9J2ubUqQKBgQCk6oKJJbZfJV/CJaz6qBFCOqrkmlD5lQ/ghOUf +EUYi0GVqYHH0vNJtz5EqEx9R7GPFNGLrGRi4z1QLJF1HD9dioJuWZujjq/NgtnG6 +bgSXJqJc52Lc4wB99AyfuL2ihSrTFmjSRx7Puc9241hTha7Rgh+vNOkq2HsH9FR3 +TTRv+QKBgG5ph+SFenSE7MgYXm2NRfG1k8bp86hrt9C8vHJ7DSO2Rr833RtqEiDJ +nD4FbR0IObaBpS2VJdOn/jBYXCG0hFuj+Shxiyg/mZN0fwPVaRWDls7jzqqPsA+b +x3XKRAn57LY8UbsNpOIqZ8kjVLPZhgfYwfOI3yAeSMv4ZnRY/MWe +-----END RSA PRIVATE KEY----- diff --git a/t/certs/public.pem b/t/certs/public.pem new file mode 100644 index 000000000000..f122f85bb735 --- /dev/null +++ b/t/certs/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2 +VFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J +BUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C +lrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA +M+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1 ++7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc +BQIDAQAB +-----END PUBLIC KEY----- From ed628b2aaaaaf9ad26c096586d1a3e56c29fb8be Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 13:46:45 +0530 Subject: [PATCH 06/25] vault integration tests with corner cases --- t/plugin/jwt-auth-vault.t | 402 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 t/plugin/jwt-auth-vault.t diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t new file mode 100644 index 000000000000..42a4166a5334 --- /dev/null +++ b/t/plugin/jwt-auth-vault.t @@ -0,0 +1,402 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + + server { + listen 8777; + + location /secure-endpoint { + content_by_lua_block { + ngx.say("successfully invoked secure endpoint") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: schema - if public and private key are not provided for RS256 +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + local conf = { + key = "key-1", + algorithm = "RS256" + } + + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end + } + } +--- response_body +failed to validate dependent schema for "algorithm": value should match only one schema, but matches none + + + +=== TEST 2: schema - vault config enabled, but vault path doesn't contains secret. +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + local conf = { + key = "key-1", + algorithm = "RS256", + vault = {} + } + + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end + } + } +--- response_body +missing valid public key + + + +=== TEST 3: store rsa key pair into vault kv/apisix/rsa/key1 +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/rsa/key1 private_key=prikey public_key=pubkey +--- response_body +Success! Data written to: kv/apisix/rsa/key1 + + + +=== TEST 4: keypair fetched from vault +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + local conf = { + key = "key-1", + algorithm = "RS256", + vault = { + path = "kv/apisix/rsa/key1", + add_prefix = false + } + } + + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end + } + } +--- response_body +ok + + + +=== TEST 5: store only private key into vault kv/apisix/rsa/key2 +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/rsa/key2 private_key=prikey +--- response_body +Success! Data written to: kv/apisix/rsa/key2 + + + +=== TEST 6: private key fetched from vault and public key from config +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + local conf = { + key = "key-1", + algorithm = "RS256", + public_key = "pubkey", + vault = { + path = "kv/apisix/rsa/key1", + add_prefix = false + } + } + + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end + } + } +--- response_body +ok + + + +=== TEST 7: preparing test 7, deleting any kv stored into path kv/apisix/jwt-auth/key/key-hs256 +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv delete kv/apisix/jwt-auth/key/key-hs256 +--- response_body +Success! Data deleted (if it existed) at: kv/apisix/jwt-auth/key/key-hs256 + + + +=== TEST 8: HS256, generate and store key into vault +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + local conf = { + key = "key-hs256", + algorithm = "HS256", + vault = { + } + } + + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("vault-path: ", conf.vault.path) + ngx.say("redacted-secret: ", conf.secret) + end + } + } +--- response_body +vault-path: jwt-auth/key/key-hs256 +redacted-secret: + + + +=== TEST 9: check generated key from test 8 - hs256 self generated kv path for vault +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv get kv/apisix/jwt-auth/key/key-hs256 +--- response_body eval +qr/===== Data ===== +Key Value +--- ----- +secret [a-zA-Z0-9+\\\/]+={0,2}/ + + + +=== TEST 10: store a secret for creating a consumer into some random path +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/some/random/path secret=apisix +--- response_body +Success! Data written to: kv/some/random/path + + + +=== TEST 11: create a consumer with plugin and username +--- 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": { + "jwt-auth": { + "key": "user-key-vault", + "algorithm": "HS256", + "vault":{ + "path": "kv/some/random/path", + "add_prefix": false + } + } + } + }]], + [[{ + "node": { + "value": { + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key-vault", + "algorithm": "HS256", + "vault":{ + "path": "kv/some/random/path", + "add_prefix": false + } + } + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 12: enable jwt auth plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "jwt-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:8777": 1 + }, + "type": "roundrobin" + }, + "uri": "/secure-endpoint" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 13: sign a jwt and access/verify /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key-vault', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + ngx.status = code + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint + + + +=== TEST 14: store rsa key pairs into vault from local filesystem +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/rsa/keypair1 public_key=@t/certs/public.pem private_key=@t/certs/private.pem +--- response_body +Success! Data written to: kv/rsa/keypair1 + + + +=== TEST 15 create consumer for RS256 algorithm with keypair fetched from 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": { + "jwt-auth": { + "key": "rsa-keypair-vault", + "algorithm": "RS256", + "vault":{ + "path": "kv/rsa/keypair1", + "add_prefix": false + } + } + } + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 16: sign a jwt with with rsa keypair and access /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa-keypair-vault', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + ngx.status = code + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint + From 9ec682ac3ecc40ab6d03cdce07acf9cbee61df47 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 13:47:17 +0530 Subject: [PATCH 07/25] minor updates --- apisix/core/vault.lua | 2 +- conf/config-default.yaml | 2 +- t/plugin/jwt-auth-vault.t | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua index 813882da2762..564e86297e62 100644 --- a/apisix/core/vault.lua +++ b/apisix/core/vault.lua @@ -52,7 +52,7 @@ local function make_request_to_vault(method, key, rel_path, data) req_addr = req_addr .. norm_path("/v1/" .. vault.prefix .. "/" .. key) else - req_addr = req_addr .. norm_path("/" .. key) + req_addr = req_addr .. norm_path("/v1/" .. key) end local res, err = httpc:request_uri(req_addr, { diff --git a/conf/config-default.yaml b/conf/config-default.yaml index e7c90024d9c5..69d191c962ef 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -283,7 +283,7 @@ etcd: # storage backend for sensitive data storage and retrieval vault: - host: "http://127.0.0.1:8200" # The host address where the vault server is running. + host: "http://0.0.0.0:8200" # The host address where the vault server is running. timeout: 10 # request timeout 30 seconds prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforce diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 42a4166a5334..8b59a180a947 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -399,4 +399,3 @@ passed } --- response_body successfully invoked secure endpoint - From 36f014125ab34210e30a0d27031c5c2e9e49a8c3 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 14:00:56 +0530 Subject: [PATCH 08/25] adding real vault server into CIs --- ci/centos7-ci.sh | 3 +++ ci/common.sh | 6 ++++++ ci/linux-ci-init-service.sh | 2 ++ ci/linux_openresty_common_runner.sh | 5 ++++- ci/pod/docker-compose.yml | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index f5a17996dc6f..c620417647bf 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -40,6 +40,9 @@ install_dependencies() { cp ./etcd-v3.4.0-linux-amd64/etcdctl /usr/local/bin/ rm -rf etcd-v3.4.0-linux-amd64 + # install vault cli capabilities + install_vault_cli + # install test::nginx yum install -y cpanminus perl cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/common.sh b/ci/common.sh index f27583b3b495..ed6c4d3b1a73 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -39,4 +39,10 @@ install_grpcurl () { tar -xvf grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz -C /usr/local/bin } +install_vault_cli () { + VAULT_VERSION="1.9.0" + wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip + unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin +} + GRPC_SERVER_EXAMPLE_VER=20210819 diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 0c3ff5d03096..b7523c1829dc 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -31,3 +31,5 @@ docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test2 -c DefaultCluster docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster + +docker exec -i vault sh -c "VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv" diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 7916d1f95bdc..98a9be25576a 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -54,8 +54,11 @@ do_install() { CGO_ENABLED=0 go build cd ../../ - # installing grpcurl + # install grpcurl install_grpcurl + + # install vault cli capabilities + install_vault_cli } script() { diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 2dedaf9dff80..911a71f7c3dd 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -136,6 +136,23 @@ services: consul_net: + ## HashiCorp Vault + vault: + image: vault:1.9.0 + container_name: vault + restart: unless-stopped + ports: + - "8900:8200" + cap_add: + - IPC_LOCK + environment: + VAULT_DEV_ROOT_TOKEN_ID: root + VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 + command: [ "vault", "server", "-dev" ] + networks: + vault_net: + + ## OpenLDAP openldap: image: bitnami/openldap:2.5.8 @@ -386,3 +403,4 @@ networks: nacos_net: skywalk_net: rocketmq_net: + vault_net: From c3aaf8fe271ee2d057e1b8dbcb4fc4336087a7b2 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 14:01:39 +0530 Subject: [PATCH 09/25] lint fix --- ci/common.sh | 2 +- t/plugin/jwt-auth-vault.t | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/common.sh b/ci/common.sh index ed6c4d3b1a73..ec8b7e6ae6c5 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -42,7 +42,7 @@ install_grpcurl () { install_vault_cli () { VAULT_VERSION="1.9.0" wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip - unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin + unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin } GRPC_SERVER_EXAMPLE_VER=20210819 diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 8b59a180a947..13200f8281ec 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -323,7 +323,7 @@ passed ngx.say(err) return end - + local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) @@ -389,7 +389,7 @@ passed ngx.say(err) return end - + local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) From 80358b94b00c61b3ad4340849473c39a1d5e4ac4 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 14:08:36 +0530 Subject: [PATCH 10/25] suggestions --- apisix/core/vault.lua | 4 ++-- apisix/plugins/jwt-auth.lua | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua index 564e86297e62..d327b1ae4853 100644 --- a/apisix/core/vault.lua +++ b/apisix/core/vault.lua @@ -93,7 +93,7 @@ local function set(key, data, rel_path) return nil, "failed to store data into vault kv engine " .. err end - return {status = "success"} + return true end _M.set = set @@ -108,7 +108,7 @@ local function delete(key, rel_path) return nil, "failed to delete data into vault kv engine " .. err end - return {status = "success"} + return true end _M.delete = delete diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 5504b329d8cc..b94061a20a29 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -315,7 +315,7 @@ local function sign_jwt_with_HS(key, auth_conf, payload) local auth_secret, err = get_secret(auth_conf) if not auth_secret then core.log.error("failed to sign jwt, err: ", err) - core.response.exit(500, "failed to sign jwt") + core.response.exit(503, "failed to sign jwt") end local ok, jwt_token = pcall(jwt.sign, _M, auth_secret, @@ -339,7 +339,7 @@ local function sign_jwt_with_RS256(key, auth_conf, payload) local public_key, private_key, err = get_rsa_keypair(auth_conf) if not public_key then core.log.error("failed to sign jwt, err: ", err) - core.response.exit(500, "failed to sign jwt") + core.response.exit(503, "failed to sign jwt") end local ok, jwt_token = pcall(jwt.sign, _M, @@ -420,6 +420,7 @@ function _M.rewrite(conf, ctx) local auth_secret, err = algorithm_handler(consumer) if not auth_secret then core.log.error("failed to retrive secrets, err: ", err) + return 503, {message = "failed to verify jwt"} end jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj) core.log.info("jwt object: ", core.json.delay_encode(jwt_obj)) From e4d10da767b03133d2a116276346cdbf3b4d3055 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 14:11:07 +0530 Subject: [PATCH 11/25] now get doesnot returns vault data --- apisix/admin/consumers.lua | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/apisix/admin/consumers.lua b/apisix/admin/consumers.lua index b5bd3e2da61f..46b23de09bdb 100644 --- a/apisix/admin/consumers.lua +++ b/apisix/admin/consumers.lua @@ -18,7 +18,6 @@ local core = require("apisix.core") local plugins = require("apisix.admin.plugins") local utils = require("apisix.admin.utils") local plugin = require("apisix.plugin") -local vault = require("apisix.core.vault") local pairs = pairs local _M = { @@ -103,33 +102,6 @@ function _M.get(consumer_name) end utils.fix_count(res.body, consumer_name) - - if consumer_name then - -- if data is queried for a single consumer, and there is any plugin where the vault config - -- is enabled - it fetches vault data and returns combined with etcd response. - local vault_fetch = {} - local attach_response = false - local _plugins = res.body.node.value.plugins or {} - for plugin_name, _schema in pairs(_plugins) do - if _schema.vault then - local res, err = vault.get(_schema.vault.path, _schema.vault.add_prefix) - if not res then - core.log.error("failed to get data from vault for plugin: ", plugin_name, - "err: ", err) - else - attach_response = true - vault_fetch[plugin_name] = res.data - end - end - end - - if attach_response then - res.body.vault = { - ["data-fetched"] = vault_fetch - } - end - end - return res.status, res.body end From f927fb9f4ccac36dea85f4660f41cdeaf5c75561 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 15:09:31 +0530 Subject: [PATCH 12/25] update exposed port address --- ci/pod/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 911a71f7c3dd..a0f4180ab37a 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -142,7 +142,7 @@ services: container_name: vault restart: unless-stopped ports: - - "8900:8200" + - "8200:8200" cap_add: - IPC_LOCK environment: From ee251aa786174af55d0ee9194dedce5f02270bab Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 23:18:30 +0530 Subject: [PATCH 13/25] documentation --- conf/config-default.yaml | 2 +- docs/en/latest/plugins/jwt-auth.md | 123 ++++++++++++++++++++++++++--- 2 files changed, 114 insertions(+), 11 deletions(-) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 69d191c962ef..0d86394de806 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -286,7 +286,7 @@ vault: host: "http://0.0.0.0:8200" # The host address where the vault server is running. timeout: 10 # request timeout 30 seconds prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored - # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforce + # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforcement of # policies, generate limited scoped tokens and tightly control the data that can be accessed # from APISIX. diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index 1ed30cedcc40..a3bb1ed32cf7 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -23,14 +23,17 @@ title: jwt-auth ## Summary -- [**Name**](#name) -- [**Attributes**](#attributes) -- [**API**](#api) -- [**How To Enable**](#how-to-enable) -- [**Test Plugin**](#test-plugin) - - [get the token in `jwt-auth` plugin:](#get-the-token-in-jwt-auth-plugin) - - [try request with token](#try-request-with-token) -- [**Disable Plugin**](#disable-plugin) +- [Summary](#summary) +- [Name](#name) +- [Attributes](#attributes) + - [Vault Plugin Attributes](#vault-plugin-attributes) +- [API](#api) +- [How To Enable](#how-to-enable) + - [Enable jwt-auth with Vault Compatibility](#enable-jwt-auth-with-vault-compatibility) +- [Test Plugin](#test-plugin) + - [Get the Token in `jwt-auth` Plugin:](#get-the-token-in-jwt-auth-plugin) + - [Try Request with Token](#try-request-with-token) +- [Disable Plugin](#disable-plugin) ## Name @@ -40,6 +43,8 @@ The `consumer` then adds its key to the query string parameter, request header, For more information on JWT, refer to [JWT](https://jwt.io/) for more information. +`jwt-auth` plugin can be integrated with HashiCorp Vault for storing and fetching secrets, RSA key pairs from its encrypted kv engine. See the examples below to have a overview of how things works. + ## Attributes | Name | Type | Requirement | Default | Valid | Description | @@ -51,6 +56,16 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio | algorithm | string | optional | "HS256" | ["HS256", "HS512", "RS256"] | encryption algorithm. | | exp | integer | optional | 86400 | [1,...] | token's expire time, in seconds | | base64_secret | boolean | optional | false | | whether secret is base64 encoded | +| vault | dictionary | optional | | | whether vault to be used for secret or public key and private key could be referenced from vault storage engine. ( see vault config here ) | + +### Vault Plugin Attributes + +To enable vault plugin, first visit the [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) and update the yaml vault attributes with your vault server configuration. + +| Name | Type | Requirement | Default | Valid | Description | +|:--------------|:--------|:------------|:--------|:----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| vault -> path | string | optional | | | If path is specified, vault uses this kv engine path for storing and retrival of secrets, public and private key. Else the plugin uses default path as `kv/apisix/jwt-auth/key/`. | +| vault -> add_prefix | boolean | optional | true | | we suggests storing keys related to APISIX under kv/apisix namespace (can be configured with config-default.yaml vault.prefix field) for better key management, policy setup etc. If the field is disabled, the vault path specified under the consumer config will be treated as absolute path - for retrival and storing secrets the vault.prefix in yaml config won't be appeneded. | ## API @@ -110,6 +125,94 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +### Enable jwt-auth with Vault Compatibility + +Sometimes, it's quite natural in production to have a centralized key management solution like vault where you don't have to update the APISIX consumer each time some part of your organization changes the signing secret key (secret for HS256/HS512 or public_key and private_key for RS256). APISIX got you covered here. The `jwt-auth` is capable of referencing keys from vault. + +**Note**: For early version of this integration support, the plugin expects the key name of secrets stored into the vault path is among [ `secret`, `public_key`, `private_key` ] to successfully use the key. In next release we are going to add the support of referencing custom named keys. + +To enable vault compatibility, just add the empty vault object (minimalistic configuration) inside the jwt-auth plugin. + +1. use vault for HS256 keystore. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "vault": {} + } + } +}' +``` + +As no secret key is provided for HS256 algorithm, the plugin generates one and store it into vault kv engine having path `/jwt-auth/key/user-key` with data `secret=<16 byte hex encoded string>`. + +2. You have stored signing secret in some path inside vault and you want to use it. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "vault": { + "path": "kv/some/random/path", + "add_prefix": false + } + } + } +}' +``` + +Here the plugin looks up for key `secret` inside vault path (`kv/some/random/path`) mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin generates a hex encoded string and store that into the same path (same as option 1 inside [here](#enable-jwt-auth-with-vault-compatibility)). + +3. RS256 rsa keypairs, both public and private keys are stored into vault. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "algorithm": "RS256", + "vault": { + "path": "kv/some/random/path", + "add_prefix": false + } + } + } +}' +``` + +The plugin looks up for `public_key` and `private_key` keys inside vault kv path mentioned inside plugin vault configuration. If not found, it returns a key not found error. + +4. public key in consumer configuration, while the private key is in vault. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "user-key", + "algorithm": "RS256", + "public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----" + "vault": { + "path": "kv/some/random/path", + "add_prefix": false + } + } + } +}' +``` + +This plugin uses rsa public key from consumer configuration and uses the private key directly fetched from vault. + You can use [APISIX Dashboard](https://github.com/apache/apisix-dashboard) to complete the above operations through the web console. 1. Add a Consumer through the web console: @@ -125,7 +228,7 @@ then add jwt-auth plugin in the Consumer page: ## Test Plugin -#### get the token in `jwt-auth` plugin: +#### Get the Token in `jwt-auth` Plugin: * without extension payload: @@ -155,7 +258,7 @@ Server: APISIX/2.4 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY ``` -#### try request with token +#### Try Request with Token * without token: From 6158837daffc3bed118d4737725280c5b6214350 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Thu, 9 Dec 2021 23:21:28 +0530 Subject: [PATCH 14/25] blank commit From f9cdc4e139ce5e5ca1eeecd2099d53e32bf55122 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 09:58:35 +0530 Subject: [PATCH 15/25] remove custom path support from mvp --- apisix/core/vault.lua | 28 ++++++---- apisix/plugins/jwt-auth.lua | 31 ++++------- docs/en/latest/plugins/jwt-auth.md | 47 ++++++----------- t/plugin/jwt-auth-vault.t | 85 +++++++++++------------------- 4 files changed, 73 insertions(+), 118 deletions(-) diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua index d327b1ae4853..f98926d77491 100644 --- a/apisix/core/vault.lua +++ b/apisix/core/vault.lua @@ -37,7 +37,7 @@ local function fetch_vault_conf() end -local function make_request_to_vault(method, key, rel_path, data) +local function make_request_to_vault(method, key, skip_prefix, data) local vault, err = fetch_vault_conf() if not vault then return nil, err @@ -48,7 +48,7 @@ local function make_request_to_vault(method, key, rel_path, data) httpc:set_timeout((vault.timeout or 5)*1000) local req_addr = vault.host - if rel_path then + if not skip_prefix then req_addr = req_addr .. norm_path("/v1/" .. vault.prefix .. "/" .. key) else @@ -69,11 +69,13 @@ local function make_request_to_vault(method, key, rel_path, data) return res.body end --- key is the vault kv engine path, joined with config yaml vault prefix -local function get(key, rel_path) +-- key is the vault kv engine path, joined with config yaml vault prefix. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for fetching data. +local function get(key, skip_prefix) core.log.info("fetching data from vault for key: ", key) - local res, err = make_request_to_vault("GET", key, rel_path) + local res, err = make_request_to_vault("GET", key, skip_prefix) if not res or err then return nil, "failed to retrtive data from vault kv engine " .. err end @@ -83,12 +85,14 @@ end _M.get = get --- key is the vault kv engine path, data is json key vaule pair -local function set(key, data, rel_path) +-- key is the vault kv engine path, data is json key vaule pair. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for storing data. +local function set(key, data, skip_prefix) core.log.info("stroing data into vault for key: ", key, "and value: ", core.json.delay_encode(data, true)) - local res, err = make_request_to_vault("POST", key, rel_path, data) + local res, err = make_request_to_vault("POST", key, skip_prefix, data) if not res or err then return nil, "failed to store data into vault kv engine " .. err end @@ -98,11 +102,13 @@ end _M.set = set --- key is the vault kv engine path, joined with config yaml vault prefix -local function delete(key, rel_path) +-- key is the vault kv engine path, joined with config yaml vault prefix. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for deleting data. +local function delete(key, skip_prefix) core.log.info("deleting data from vault for key: ", key) - local res, err = make_request_to_vault("DELETE", key, rel_path) + local res, err = make_request_to_vault("DELETE", key, skip_prefix) if not res or err then return nil, "failed to delete data into vault kv engine " .. err diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index b94061a20a29..9e23a4801d68 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -29,7 +29,7 @@ local ngx_time = ngx.time local sub_str = string.sub local plugin_name = "jwt-auth" local pcall = pcall - +local jwt_vault_prefix = "jwt-auth/keys/" local lrucache = core.lrucache.new({ type = "plugin", @@ -58,10 +58,7 @@ local consumer_schema = { }, vault = { type = "object", - properties = { - path = {type = "string"}, - add_prefix = {type = "boolean"} - } + properties = {} } }, dependencies = { @@ -89,10 +86,7 @@ local consumer_schema = { properties = { vault = { type = "object", - properties = { - path = {type = "string"}, - add_prefix = {type = "boolean"} - } + properties = {} }, algorithm = { enum = {"RS256"}, @@ -157,15 +151,10 @@ function _M.check_schema(conf, schema_type) end local vout = {} + local vault_path = jwt_vault_prefix .. conf.key if conf.vault then - -- create vault path, if not set by admin. - if not conf.vault.path then - conf.vault.path = "jwt-auth/key/" .. conf.key - conf.vault.add_prefix = true - end - -- fetch the data to check if the keys are stored into vault - local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + local res, err = vault.get(vault_path) if not res or err then core.log.error("failed to fetch data from vault: ", err) return false, "error while fetching data from vault, " .. @@ -184,14 +173,14 @@ function _M.check_schema(conf, schema_type) -- if vault config is enabled, lifecycle of the -- HS256/HS512 secret will be externally managed by vault. if conf.vault then - local res, err = vault.set(conf.vault.path, { + local res, err = vault.set(vault_path, { secret = secret, - }, conf.vault.add_prefix) + }) if not res or err then core.log.error("failed to put data into vault: ", err) return false, "error communicating with vault server" end - conf.secret = "" + conf.secret = "" else conf.secret = secret end @@ -247,7 +236,7 @@ end local function get_secret(conf) local secret = conf.secret if conf.vault then - local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + local res, err = vault.get(jwt_vault_prefix .. conf.key) if not res or err then return nil, err end @@ -276,7 +265,7 @@ local function get_rsa_keypair(conf) local vout = {} if conf.vault then - local res, err = vault.get(conf.vault.path, conf.vault.add_prefix) + local res, err = vault.get(jwt_vault_prefix .. conf.key) if not res or err then return nil, nil, err end diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index a3bb1ed32cf7..25f7e7bf87eb 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -26,7 +26,6 @@ title: jwt-auth - [Summary](#summary) - [Name](#name) - [Attributes](#attributes) - - [Vault Plugin Attributes](#vault-plugin-attributes) - [API](#api) - [How To Enable](#how-to-enable) - [Enable jwt-auth with Vault Compatibility](#enable-jwt-auth-with-vault-compatibility) @@ -43,7 +42,7 @@ The `consumer` then adds its key to the query string parameter, request header, For more information on JWT, refer to [JWT](https://jwt.io/) for more information. -`jwt-auth` plugin can be integrated with HashiCorp Vault for storing and fetching secrets, RSA key pairs from its encrypted kv engine. See the examples below to have a overview of how things works. +`jwt-auth` plugin can be integrated with HashiCorp Vault for storing and fetching secrets, RSA key pairs from its encrypted kv engine. See the [examples](#enable-jwt-auth-with-vault-compatibility) below to have an overview of how things work. ## Attributes @@ -56,16 +55,9 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio | algorithm | string | optional | "HS256" | ["HS256", "HS512", "RS256"] | encryption algorithm. | | exp | integer | optional | 86400 | [1,...] | token's expire time, in seconds | | base64_secret | boolean | optional | false | | whether secret is base64 encoded | -| vault | dictionary | optional | | | whether vault to be used for secret or public key and private key could be referenced from vault storage engine. ( see vault config here ) | +| vault | dictionary | optional | | | whether vault to be used for secret (secret for HS256/HS512 or public_key and private_key for RS256) storage and retrieval. The plugin by default uses the vault path as `kv/apisix/jwt-auth/keys/` for secret retrieval. | -### Vault Plugin Attributes - -To enable vault plugin, first visit the [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) and update the yaml vault attributes with your vault server configuration. - -| Name | Type | Requirement | Default | Valid | Description | -|:--------------|:--------|:------------|:--------|:----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| -| vault -> path | string | optional | | | If path is specified, vault uses this kv engine path for storing and retrival of secrets, public and private key. Else the plugin uses default path as `kv/apisix/jwt-auth/key/`. | -| vault -> add_prefix | boolean | optional | true | | we suggests storing keys related to APISIX under kv/apisix namespace (can be configured with config-default.yaml vault.prefix field) for better key management, policy setup etc. If the field is disabled, the vault path specified under the consumer config will be treated as absolute path - for retrival and storing secrets the vault.prefix in yaml config won't be appeneded. | +**Note**: To enable vault integration, first visit the [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) and update the yaml vault attributes with your vault server configuration. ## API @@ -127,11 +119,11 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ### Enable jwt-auth with Vault Compatibility -Sometimes, it's quite natural in production to have a centralized key management solution like vault where you don't have to update the APISIX consumer each time some part of your organization changes the signing secret key (secret for HS256/HS512 or public_key and private_key for RS256). APISIX got you covered here. The `jwt-auth` is capable of referencing keys from vault. +Sometimes, it's quite natural in production to have a centralized key management solution like vault where you don't have to update the APISIX consumer each time some part of your organization changes the signing secret key (secret for HS256/HS512 or public_key and private_key for RS256) and/or for privacy concerns you don't want to use the key through APISIX admin APIs. APISIX got you covered here. The `jwt-auth` is capable of referencing keys from vault. **Note**: For early version of this integration support, the plugin expects the key name of secrets stored into the vault path is among [ `secret`, `public_key`, `private_key` ] to successfully use the key. In next release we are going to add the support of referencing custom named keys. -To enable vault compatibility, just add the empty vault object (minimalistic configuration) inside the jwt-auth plugin. +To enable vault compatibility, just add the empty vault object inside the jwt-auth plugin. 1. use vault for HS256 keystore. @@ -148,9 +140,9 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -As no secret key is provided for HS256 algorithm, the plugin generates one and store it into vault kv engine having path `/jwt-auth/key/user-key` with data `secret=<16 byte hex encoded string>`. +As no secret key is provided for HS256 algorithm, the plugin generates one and store it into vault kv engine having path `/jwt-auth/keys/user-key` with data `secret=<16 byte hex encoded string>`. -2. You have stored signing secret in some path inside vault and you want to use it. +1. You have stored signing secret inside vault and you want to use it. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -158,19 +150,16 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 "username": "jack", "plugins": { "jwt-auth": { - "key": "user-key", - "vault": { - "path": "kv/some/random/path", - "add_prefix": false - } + "key": "key-1", + "vault": {} } } }' ``` -Here the plugin looks up for key `secret` inside vault path (`kv/some/random/path`) mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin generates a hex encoded string and store that into the same path (same as option 1 inside [here](#enable-jwt-auth-with-vault-compatibility)). +Here the plugin looks up for key `secret` inside vault path (`/jwt-auth/keys/key-1`) for key `key-1` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin generates a random hex encoded string and store that into the same path (same as option 1 inside [here](#enable-jwt-auth-with-vault-compatibility)). -3. RS256 rsa keypairs, both public and private keys are stored into vault. +1. RS256 rsa keypairs, both public and private keys are stored into vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -178,18 +167,15 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 "username": "jack", "plugins": { "jwt-auth": { - "key": "user-key", + "key": "rsa-keypair", "algorithm": "RS256", - "vault": { - "path": "kv/some/random/path", - "add_prefix": false - } + "vault": {} } } }' ``` -The plugin looks up for `public_key` and `private_key` keys inside vault kv path mentioned inside plugin vault configuration. If not found, it returns a key not found error. +The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/jwt-auth/keys/rsa-keypair`) for key `rsa-keypair` mentioned inside plugin vault configuration. If not found, it returns a key not found error. 4. public key in consumer configuration, while the private key is in vault. @@ -202,10 +188,7 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 "key": "user-key", "algorithm": "RS256", "public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----" - "vault": { - "path": "kv/some/random/path", - "add_prefix": false - } + "vault": {} } } }' diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 13200f8281ec..4345c2ce7ae3 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -100,11 +100,11 @@ missing valid public key -=== TEST 3: store rsa key pair into vault kv/apisix/rsa/key1 +=== TEST 3: store rsa key pair into vault kv/apisix/jwt-auth/keys/key1 --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/rsa/key1 private_key=prikey public_key=pubkey +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key1 private_key=prikey public_key=pubkey --- response_body -Success! Data written to: kv/apisix/rsa/key1 +Success! Data written to: kv/apisix/jwt-auth/keys/key1 @@ -115,12 +115,9 @@ Success! Data written to: kv/apisix/rsa/key1 local plugin = require("apisix.plugins.jwt-auth") local core = require("apisix.core") local conf = { - key = "key-1", + key = "key1", algorithm = "RS256", - vault = { - path = "kv/apisix/rsa/key1", - add_prefix = false - } + vault = {} } local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) @@ -136,11 +133,11 @@ ok -=== TEST 5: store only private key into vault kv/apisix/rsa/key2 +=== TEST 5: store only private key into vault kv/apisix/jwt-auth/keys/key1 --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/rsa/key2 private_key=prikey +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key1 private_key=prikey --- response_body -Success! Data written to: kv/apisix/rsa/key2 +Success! Data written to: kv/apisix/jwt-auth/keys/key1 @@ -151,12 +148,10 @@ Success! Data written to: kv/apisix/rsa/key2 local plugin = require("apisix.plugins.jwt-auth") local core = require("apisix.core") local conf = { - key = "key-1", + key = "key1", algorithm = "RS256", public_key = "pubkey", vault = { - path = "kv/apisix/rsa/key1", - add_prefix = false } } @@ -173,11 +168,12 @@ ok -=== TEST 7: preparing test 7, deleting any kv stored into path kv/apisix/jwt-auth/key/key-hs256 +=== TEST 7: preparing test 7, deleting (if any) any kv stored into path kv/apisix/jwt-auth/keys/key-hs256 +If the path contains a key named `secret`, plugin won't generate a new hex encoded secret. --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv delete kv/apisix/jwt-auth/key/key-hs256 +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv delete kv/apisix/jwt-auth/keys/key-hs256 --- response_body -Success! Data deleted (if it existed) at: kv/apisix/jwt-auth/key/key-hs256 +Success! Data deleted (if it existed) at: kv/apisix/jwt-auth/keys/key-hs256 @@ -198,20 +194,18 @@ Success! Data deleted (if it existed) at: kv/apisix/jwt-auth/key/key-hs256 if not ok then ngx.say(err) else - ngx.say("vault-path: ", conf.vault.path) ngx.say("redacted-secret: ", conf.secret) end } } --- response_body -vault-path: jwt-auth/key/key-hs256 -redacted-secret: +redacted-secret: === TEST 9: check generated key from test 8 - hs256 self generated kv path for vault --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv get kv/apisix/jwt-auth/key/key-hs256 +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv get kv/apisix/jwt-auth/keys/key-hs256 --- response_body eval qr/===== Data ===== Key Value @@ -220,15 +214,7 @@ secret [a-zA-Z0-9+\\\/]+={0,2}/ -=== TEST 10: store a secret for creating a consumer into some random path ---- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/some/random/path secret=apisix ---- response_body -Success! Data written to: kv/some/random/path - - - -=== TEST 11: create a consumer with plugin and username +=== TEST 10: create a consumer with plugin and username --- config location /t { content_by_lua_block { @@ -239,12 +225,9 @@ Success! Data written to: kv/some/random/path "username": "jack", "plugins": { "jwt-auth": { - "key": "user-key-vault", + "key": "key-hs256", "algorithm": "HS256", - "vault":{ - "path": "kv/some/random/path", - "add_prefix": false - } + "vault":{} } } }]], @@ -254,12 +237,9 @@ Success! Data written to: kv/some/random/path "username": "jack", "plugins": { "jwt-auth": { - "key": "user-key-vault", + "key": "key-hs256", "algorithm": "HS256", - "vault":{ - "path": "kv/some/random/path", - "add_prefix": false - } + "vault":{} } } } @@ -277,7 +257,7 @@ passed -=== TEST 12: enable jwt auth plugin using admin api +=== TEST 11: enable jwt auth plugin using admin api --- config location /t { content_by_lua_block { @@ -309,12 +289,12 @@ passed -=== TEST 13: sign a jwt and access/verify /secure-endpoint +=== TEST 12: sign a jwt and access/verify /secure-endpoint --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, err, sign = t('/apisix/plugin/jwt/sign?key=user-key-vault', + local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256', ngx.HTTP_GET ) @@ -336,15 +316,15 @@ successfully invoked secure endpoint -=== TEST 14: store rsa key pairs into vault from local filesystem +=== TEST 13: store rsa key pairs into vault from local filesystem --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/rsa/keypair1 public_key=@t/certs/public.pem private_key=@t/certs/private.pem +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa public_key=@t/certs/public.pem private_key=@t/certs/private.pem --- response_body -Success! Data written to: kv/rsa/keypair1 +Success! Data written to: kv/apisix/jwt-auth/keys/rsa -=== TEST 15 create consumer for RS256 algorithm with keypair fetched from vault +=== TEST 14 create consumer for RS256 algorithm with keypair fetched from vault --- config location /t { content_by_lua_block { @@ -355,12 +335,9 @@ Success! Data written to: kv/rsa/keypair1 "username": "jack", "plugins": { "jwt-auth": { - "key": "rsa-keypair-vault", + "key": "rsa", "algorithm": "RS256", - "vault":{ - "path": "kv/rsa/keypair1", - "add_prefix": false - } + "vault":{} } } }]] @@ -375,12 +352,12 @@ passed -=== TEST 16: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 15: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa-keypair-vault', + local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa', ngx.HTTP_GET ) From 67291068227a0619f5643658721bf1d6bf0472c8 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 10:54:18 +0530 Subject: [PATCH 16/25] trimming down validation and key generation if vault config is enabled --- apisix/plugins/jwt-auth.lua | 27 +++- docs/en/latest/plugins/jwt-auth.md | 27 +--- t/plugin/jwt-auth-vault.t | 192 +++++++++++------------------ 3 files changed, 98 insertions(+), 148 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 9e23a4801d68..bfc976c70695 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -144,11 +144,29 @@ function _M.check_schema(conf, schema_type) return false, err end - -- in nginx init_worker_by_lua context API calls are disabled, - -- also that is a costly operation during system startup. - if ngx.get_phase() == "init_worker" then + if conf.vault then + core.log.info("skipping jwt-auth schema validation with vault") return true end + if conf.algorithm ~= "RS256" and not conf.secret then + conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) + elseif conf.base64_secret then + if ngx_decode_base64(conf.secret) == nil then + return false, "base64_secret required but the secret is not in base64 format" + end + end + + if conf.algorithm == "RS256" then + -- Possible options are a) both are in vault, b) both in schema + -- c) one in schema, another in vault. + if not conf.public_key then + return false, "missing valid public key" + end + if not conf.private_key then + return false, "missing valid private key" + end + end + local vout = {} local vault_path = jwt_vault_prefix .. conf.key @@ -193,8 +211,7 @@ function _M.check_schema(conf, schema_type) end if conf.algorithm == "RS256" then - -- check from consumer config and vault data store. Possible options are - -- a) both are in vault, b) both in schema, c) one in schema, another in vault. + if not conf.public_key and not vout.public_key then return false, "missing valid public key" end diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index 25f7e7bf87eb..460a5d0c5eb9 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -125,24 +125,7 @@ Sometimes, it's quite natural in production to have a centralized key management To enable vault compatibility, just add the empty vault object inside the jwt-auth plugin. -1. use vault for HS256 keystore. - -```shell -curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "username": "jack", - "plugins": { - "jwt-auth": { - "key": "user-key", - "vault": {} - } - } -}' -``` - -As no secret key is provided for HS256 algorithm, the plugin generates one and store it into vault kv engine having path `/jwt-auth/keys/user-key` with data `secret=<16 byte hex encoded string>`. - -1. You have stored signing secret inside vault and you want to use it. +1. You have stored HS256 signing secret inside vault and you want to use it for jwt signing and verification. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -157,9 +140,9 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -Here the plugin looks up for key `secret` inside vault path (`/jwt-auth/keys/key-1`) for key `key-1` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin generates a random hex encoded string and store that into the same path (same as option 1 inside [here](#enable-jwt-auth-with-vault-compatibility)). +Here the plugin looks up for key `secret` inside vault path (`/jwt-auth/keys/key-1`) for key `key-1` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin logs error and fails to perform jwt authentication. -1. RS256 rsa keypairs, both public and private keys are stored into vault. +2. RS256 rsa keypairs, both public and private keys are stored into vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -175,9 +158,9 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/jwt-auth/keys/rsa-keypair`) for key `rsa-keypair` mentioned inside plugin vault configuration. If not found, it returns a key not found error. +The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/jwt-auth/keys/rsa-keypair`) for key `rsa-keypair` mentioned inside plugin vault configuration. If not found, authentication fails. -4. public key in consumer configuration, while the private key is in vault. +3. public key in consumer configuration, while the private key is in vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 4345c2ce7ae3..c7a4c83b17dd 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -75,7 +75,7 @@ failed to validate dependent schema for "algorithm": value should match only one -=== TEST 2: schema - vault config enabled, but vault path doesn't contains secret. +=== TEST 2: schema - vault config enabled, it expects keys present in vault datastore. --- config location /t { content_by_lua_block { @@ -96,125 +96,11 @@ failed to validate dependent schema for "algorithm": value should match only one } } --- response_body -missing valid public key - - - -=== TEST 3: store rsa key pair into vault kv/apisix/jwt-auth/keys/key1 ---- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key1 private_key=prikey public_key=pubkey ---- response_body -Success! Data written to: kv/apisix/jwt-auth/keys/key1 - - - -=== TEST 4: keypair fetched from vault ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.jwt-auth") - local core = require("apisix.core") - local conf = { - key = "key1", - algorithm = "RS256", - vault = {} - } - - local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - else - ngx.say("ok") - end - } - } ---- response_body ok -=== TEST 5: store only private key into vault kv/apisix/jwt-auth/keys/key1 ---- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key1 private_key=prikey ---- response_body -Success! Data written to: kv/apisix/jwt-auth/keys/key1 - - - -=== TEST 6: private key fetched from vault and public key from config ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.jwt-auth") - local core = require("apisix.core") - local conf = { - key = "key1", - algorithm = "RS256", - public_key = "pubkey", - vault = { - } - } - - local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - else - ngx.say("ok") - end - } - } ---- response_body -ok - - - -=== TEST 7: preparing test 7, deleting (if any) any kv stored into path kv/apisix/jwt-auth/keys/key-hs256 -If the path contains a key named `secret`, plugin won't generate a new hex encoded secret. ---- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv delete kv/apisix/jwt-auth/keys/key-hs256 ---- response_body -Success! Data deleted (if it existed) at: kv/apisix/jwt-auth/keys/key-hs256 - - - -=== TEST 8: HS256, generate and store key into vault ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.jwt-auth") - local core = require("apisix.core") - local conf = { - key = "key-hs256", - algorithm = "HS256", - vault = { - } - } - - local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - else - ngx.say("redacted-secret: ", conf.secret) - end - } - } ---- response_body -redacted-secret: - - - -=== TEST 9: check generated key from test 8 - hs256 self generated kv path for vault ---- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv get kv/apisix/jwt-auth/keys/key-hs256 ---- response_body eval -qr/===== Data ===== -Key Value ---- ----- -secret [a-zA-Z0-9+\\\/]+={0,2}/ - - - -=== TEST 10: create a consumer with plugin and username +=== TEST 3: create a consumer with plugin and username --- config location /t { content_by_lua_block { @@ -257,7 +143,7 @@ passed -=== TEST 11: enable jwt auth plugin using admin api +=== TEST 4: enable jwt auth plugin using admin api --- config location /t { content_by_lua_block { @@ -289,7 +175,7 @@ passed -=== TEST 12: sign a jwt and access/verify /secure-endpoint +=== TEST 5: sign a jwt and access/verify /secure-endpoint --- config location /t { content_by_lua_block { @@ -316,7 +202,7 @@ successfully invoked secure endpoint -=== TEST 13: store rsa key pairs into vault from local filesystem +=== TEST 6: store rsa key pairs into vault from local filesystem --- exec VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa public_key=@t/certs/public.pem private_key=@t/certs/private.pem --- response_body @@ -324,7 +210,7 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa -=== TEST 14 create consumer for RS256 algorithm with keypair fetched from vault +=== TEST 7: create consumer for RS256 algorithm with keypair fetched from vault --- config location /t { content_by_lua_block { @@ -352,7 +238,7 @@ passed -=== TEST 15: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 8: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { @@ -376,3 +262,67 @@ passed } --- response_body successfully invoked secure endpoint + + + +=== TEST 9: store rsa private key into vault from local filesystem +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa1 private_key=@t/certs/private.pem +--- response_body +Success! Data written to: kv/apisix/jwt-auth/keys/rsa1 + + + +=== TEST 10: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema +--- 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": { + "jwt-auth": { + "key": "rsa1", + "algorithm": "RS256", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2\nVFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J\nBUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C\nlrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA\nM+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1\n+7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc\nBQIDAQAB\n-----END PUBLIC KEY-----\n", + "vault":{} + } + } + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: sign a jwt with with rsa keypair and access /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa1', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + ngx.status = code + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint From 83b3fe05bf3b4fbd846bd538f5e0a1997b432be1 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 10:57:34 +0530 Subject: [PATCH 17/25] remove redundant codes --- apisix/plugins/jwt-auth.lua | 54 +------------------------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index bfc976c70695..88e4c90edc71 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -148,6 +148,7 @@ function _M.check_schema(conf, schema_type) core.log.info("skipping jwt-auth schema validation with vault") return true end + if conf.algorithm ~= "RS256" and not conf.secret then conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) elseif conf.base64_secret then @@ -167,59 +168,6 @@ function _M.check_schema(conf, schema_type) end end - - local vout = {} - local vault_path = jwt_vault_prefix .. conf.key - if conf.vault then - -- fetch the data to check if the keys are stored into vault - local res, err = vault.get(vault_path) - if not res or err then - core.log.error("failed to fetch data from vault: ", err) - return false, "error while fetching data from vault, " .. - "please check the connection or remove vault config" - end - -- if there is no data on that path, that's absolutely fine. - vout = res.data or {} - end - - if conf.algorithm ~= "RS256" then - local secret = conf.secret or vout.secret - -- if no secret is provided, generate one. - if not secret then - secret = ngx_encode_base64(resty_random.bytes(32, true)) - - -- if vault config is enabled, lifecycle of the - -- HS256/HS512 secret will be externally managed by vault. - if conf.vault then - local res, err = vault.set(vault_path, { - secret = secret, - }) - if not res or err then - core.log.error("failed to put data into vault: ", err) - return false, "error communicating with vault server" - end - conf.secret = "" - else - conf.secret = secret - end - - elseif conf.base64_secret then - if ngx_decode_base64(secret) == nil then - return false, "base64_secret required but the secret is not in base64 format" - end - end - end - - if conf.algorithm == "RS256" then - - if not conf.public_key and not vout.public_key then - return false, "missing valid public key" - end - if not conf.private_key and not vout.private_key then - return false, "missing valid private key" - end - end - return true end From 58292d224304b120a32b79c560f51be80539b4b3 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 13:09:35 +0530 Subject: [PATCH 18/25] Ci fix --- apisix/plugins/jwt-auth.lua | 2 +- t/plugin/jwt-auth-vault.t | 55 ++++++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 88e4c90edc71..12705ac15494 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -206,7 +206,7 @@ local function get_secret(conf) return nil, err end - if not res.data and not res.data.secret then + if not res.data or not res.data.secret then return nil, "secret could not found in vault: " .. core.json.encode(res) end secret = res.data.secret diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index c7a4c83b17dd..58b554384db9 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -175,7 +175,48 @@ passed -=== TEST 5: sign a jwt and access/verify /secure-endpoint +=== TEST 5: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + ngx.status = code + ngx.print(res) + } + } +--- response_body +failed to sign jwt +--- error_code: 503 +--- error_log: true +--- grep_error_log eval +qr/failed to sign jwt, err: secret could not found in vault/ +--- grep_error_log_out +failed to sign jwt, err: secret could not found in vault + + + +=== TEST 6: store HS256 secret into vault +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key-hs256 secret=$3nsitiv3-c8d3 +--- response_body +Success! Data written to: kv/apisix/jwt-auth/keys/key-hs256 + + + +=== TEST 7: sign a HS256 jwt and access/verify /secure-endpoint --- config location /t { content_by_lua_block { @@ -202,7 +243,7 @@ successfully invoked secure endpoint -=== TEST 6: store rsa key pairs into vault from local filesystem +=== TEST 8: store rsa key pairs into vault from local filesystem --- exec VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa public_key=@t/certs/public.pem private_key=@t/certs/private.pem --- response_body @@ -210,7 +251,7 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa -=== TEST 7: create consumer for RS256 algorithm with keypair fetched from vault +=== TEST 9: create consumer for RS256 algorithm with keypair fetched from vault --- config location /t { content_by_lua_block { @@ -238,7 +279,7 @@ passed -=== TEST 8: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 10: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { @@ -265,7 +306,7 @@ successfully invoked secure endpoint -=== TEST 9: store rsa private key into vault from local filesystem +=== TEST 11: store rsa private key into vault from local filesystem --- exec VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa1 private_key=@t/certs/private.pem --- response_body @@ -273,7 +314,7 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa1 -=== TEST 10: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema +=== TEST 12: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema --- config location /t { content_by_lua_block { @@ -302,7 +343,7 @@ passed -=== TEST 11: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 13: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { From 55c105d19016c789d0f719a0ec291d95c5631320 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 17:37:59 +0530 Subject: [PATCH 19/25] changing vault kv suffix to /consumer//jwt-auth --- apisix/plugins/jwt-auth.lua | 37 +++++++++++++++++------------- conf/config-default.yaml | 4 +++- docs/en/latest/plugins/jwt-auth.md | 16 ++++++------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index 12705ac15494..bf52fa094fae 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -29,7 +29,7 @@ local ngx_time = ngx.time local sub_str = string.sub local plugin_name = "jwt-auth" local pcall = pcall -local jwt_vault_prefix = "jwt-auth/keys/" + local lrucache = core.lrucache.new({ type = "plugin", @@ -198,10 +198,15 @@ local function fetch_jwt_token(ctx) end -local function get_secret(conf) +local function get_vault_path(username) + return "consumer/".. username .. "/jwt-auth" +end + + +local function get_secret(conf, consumer_name) local secret = conf.secret if conf.vault then - local res, err = vault.get(jwt_vault_prefix .. conf.key) + local res, err = vault.get(get_vault_path(consumer_name)) if not res or err then return nil, err end @@ -220,7 +225,7 @@ local function get_secret(conf) end -local function get_rsa_keypair(conf) +local function get_rsa_keypair(conf, consumer_name) local public_key = conf.public_key local private_key = conf.private_key -- if keys are present in conf, no need to query vault (fallback) @@ -230,7 +235,7 @@ local function get_rsa_keypair(conf) local vout = {} if conf.vault then - local res, err = vault.get(jwt_vault_prefix .. conf.key) + local res, err = vault.get(get_vault_path(consumer_name)) if not res or err then return nil, nil, err end @@ -265,8 +270,8 @@ local function get_real_payload(key, auth_conf, payload) end -local function sign_jwt_with_HS(key, auth_conf, payload) - local auth_secret, err = get_secret(auth_conf) +local function sign_jwt_with_HS(key, consumer, payload) + local auth_secret, err = get_secret(consumer.auth_conf, consumer.username) if not auth_secret then core.log.error("failed to sign jwt, err: ", err) core.response.exit(503, "failed to sign jwt") @@ -276,9 +281,9 @@ local function sign_jwt_with_HS(key, auth_conf, payload) { header = { typ = "JWT", - alg = auth_conf.algorithm + alg = consumer.auth_conf.algorithm }, - payload = get_real_payload(key, auth_conf, payload) + payload = get_real_payload(key, consumer.auth_conf, payload) } ) if not ok then @@ -289,8 +294,8 @@ local function sign_jwt_with_HS(key, auth_conf, payload) end -local function sign_jwt_with_RS256(key, auth_conf, payload) - local public_key, private_key, err = get_rsa_keypair(auth_conf) +local function sign_jwt_with_RS256(key, consumer, payload) + local public_key, private_key, err = get_rsa_keypair(consumer.auth_conf, consumer.username) if not public_key then core.log.error("failed to sign jwt, err: ", err) core.response.exit(503, "failed to sign jwt") @@ -301,12 +306,12 @@ local function sign_jwt_with_RS256(key, auth_conf, payload) { header = { typ = "JWT", - alg = auth_conf.algorithm, + alg = consumer.auth_conf.algorithm, x5c = { public_key, } }, - payload = get_real_payload(key, auth_conf, payload) + payload = get_real_payload(key, consumer.auth_conf, payload) } ) if not ok then @@ -324,13 +329,13 @@ local function algorithm_handler(consumer, method_only) return sign_jwt_with_HS end - return get_secret(consumer.auth_conf) + return get_secret(consumer.auth_conf, consumer.username) elseif consumer.auth_conf.algorithm == "RS256" then if method_only then return sign_jwt_with_RS256 end - local public_key, _, err = get_rsa_keypair(consumer.auth_conf) + local public_key, _, err = get_rsa_keypair(consumer.auth_conf, consumer.username) return public_key, err end end @@ -417,7 +422,7 @@ local function gen_token() core.log.info("consumer: ", core.json.delay_encode(consumer)) local sign_handler = algorithm_handler(consumer, true) - local jwt_token = sign_handler(key, consumer.auth_conf, payload) + local jwt_token = sign_handler(key, consumer, payload) if jwt_token then return core.response.exit(200, jwt_token) end diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 0d86394de806..23baf2b57916 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -281,7 +281,9 @@ etcd: # the default value is true, e.g. the certificate will be verified strictly. #sni: # the SNI for etcd TLS requests. If missed, the host part of the URL will be used. -# storage backend for sensitive data storage and retrieval +# HashiCorp Vault storage backend for sensitive data retrieval. The config shows an example of what APISIX expects if you +# wish to integrate Vault for secret (sensetive string, public private keys etc.) retrieval. APISIX communicates with Vault +# server HTTP APIs. By default, APISIX doesn't need this configuration. vault: host: "http://0.0.0.0:8200" # The host address where the vault server is running. timeout: 10 # request timeout 30 seconds diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index 460a5d0c5eb9..db282c31b540 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -55,9 +55,9 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio | algorithm | string | optional | "HS256" | ["HS256", "HS512", "RS256"] | encryption algorithm. | | exp | integer | optional | 86400 | [1,...] | token's expire time, in seconds | | base64_secret | boolean | optional | false | | whether secret is base64 encoded | -| vault | dictionary | optional | | | whether vault to be used for secret (secret for HS256/HS512 or public_key and private_key for RS256) storage and retrieval. The plugin by default uses the vault path as `kv/apisix/jwt-auth/keys/` for secret retrieval. | +| vault | object | optional | | | whether vault to be used for secret (secret for HS256/HS512 or public_key and private_key for RS256) storage and retrieval. The plugin by default uses the vault path as `kv/apisix/consumer//jwt-auth` for secret retrieval. | -**Note**: To enable vault integration, first visit the [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) and update the yaml vault attributes with your vault server configuration. +**Note**: To enable vault integration, first visit the [config.yaml](https://github.com/apache/apisix/blob/master/conf/config.yaml) update it with your vault server configuration, host address and access token. You can take a look of what APISIX expects from the config.yaml at [default-config.yaml](https://github.com/apache/apisix/blob/master/conf/default-config.yaml) under the vault attributes. ## API @@ -121,7 +121,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 Sometimes, it's quite natural in production to have a centralized key management solution like vault where you don't have to update the APISIX consumer each time some part of your organization changes the signing secret key (secret for HS256/HS512 or public_key and private_key for RS256) and/or for privacy concerns you don't want to use the key through APISIX admin APIs. APISIX got you covered here. The `jwt-auth` is capable of referencing keys from vault. -**Note**: For early version of this integration support, the plugin expects the key name of secrets stored into the vault path is among [ `secret`, `public_key`, `private_key` ] to successfully use the key. In next release we are going to add the support of referencing custom named keys. +**Note**: For early version of this integration support, the plugin expects the key name of secrets stored into the vault path is among [ `secret`, `public_key`, `private_key` ] to successfully use the key. In future releases, we are going to add the support of referencing custom named keys. To enable vault compatibility, just add the empty vault object inside the jwt-auth plugin. @@ -140,14 +140,14 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -Here the plugin looks up for key `secret` inside vault path (`/jwt-auth/keys/key-1`) for key `key-1` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin logs error and fails to perform jwt authentication. +Here the plugin looks up for key `secret` inside vault path (`/consumer/jack/jwt-auth`) for consumer username `jack` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin logs error and fails to perform jwt authentication. -2. RS256 rsa keypairs, both public and private keys are stored into vault. +1. RS256 rsa keypairs, both public and private keys are stored into vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { - "username": "jack", + "username": "kowalski", "plugins": { "jwt-auth": { "key": "rsa-keypair", @@ -158,14 +158,14 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/jwt-auth/keys/rsa-keypair`) for key `rsa-keypair` mentioned inside plugin vault configuration. If not found, authentication fails. +The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/consumer/kowalski/jwt-auth`) for username `kowalski` mentioned inside plugin vault configuration. If not found, authentication fails. 3. public key in consumer configuration, while the private key is in vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { - "username": "jack", + "username": "rico", "plugins": { "jwt-auth": { "key": "user-key", From 1f2ff22a61b42915b73f7962a34ae5351d038f15 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 17:38:39 +0530 Subject: [PATCH 20/25] update tests and modify the way http status code were sent --- t/plugin/jwt-auth-vault.t | 121 +++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 58b554384db9..e2b1c3f7c1a7 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -51,56 +51,41 @@ run_tests; __DATA__ -=== TEST 1: schema - if public and private key are not provided for RS256 +=== TEST 1: schema check --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.jwt-auth") local core = require("apisix.core") - local conf = { - key = "key-1", - algorithm = "RS256" - } - - local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - else - ngx.say("ok") + for _, conf in ipairs({ + { + -- public and private key are not provided for RS256, returns error + key = "key-1", + algorithm = "RS256" + }, + { + -- public and private key are not provided but vault config is enabled. + key = "key-1", + algorithm = "RS256", + vault = {} + } + }) do + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end end } } --- response_body failed to validate dependent schema for "algorithm": value should match only one schema, but matches none - - - -=== TEST 2: schema - vault config enabled, it expects keys present in vault datastore. ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.jwt-auth") - local core = require("apisix.core") - local conf = { - key = "key-1", - algorithm = "RS256", - vault = {} - } - - local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - else - ngx.say("ok") - end - } - } ---- response_body ok -=== TEST 3: create a consumer with plugin and username +=== TEST 2: create a consumer with plugin and username --- config location /t { content_by_lua_block { @@ -134,7 +119,9 @@ ok }]] ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.say(body) } } @@ -143,7 +130,7 @@ passed -=== TEST 4: enable jwt auth plugin using admin api +=== TEST 3: enable jwt auth plugin using admin api --- config location /t { content_by_lua_block { @@ -175,7 +162,7 @@ passed -=== TEST 5: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault +=== TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault --- config location /t { content_by_lua_block { @@ -193,7 +180,9 @@ passed local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.print(res) } } @@ -208,15 +197,15 @@ failed to sign jwt, err: secret could not found in vault -=== TEST 6: store HS256 secret into vault +=== TEST 5: store HS256 secret into vault --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/key-hs256 secret=$3nsitiv3-c8d3 +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/jack/jwt-auth secret=$3nsitiv3-c8d3 --- response_body -Success! Data written to: kv/apisix/jwt-auth/keys/key-hs256 +Success! Data written to: kv/apisix/consumer/jack/jwt-auth -=== TEST 7: sign a HS256 jwt and access/verify /secure-endpoint +=== TEST 6: sign a HS256 jwt and access/verify /secure-endpoint --- config location /t { content_by_lua_block { @@ -234,7 +223,9 @@ Success! Data written to: kv/apisix/jwt-auth/keys/key-hs256 local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.print(res) } } @@ -243,15 +234,15 @@ successfully invoked secure endpoint -=== TEST 8: store rsa key pairs into vault from local filesystem +=== TEST 7: store rsa key pairs into vault from local filesystem --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa public_key=@t/certs/public.pem private_key=@t/certs/private.pem +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/jim/jwt-auth public_key=@t/certs/public.pem private_key=@t/certs/private.pem --- response_body -Success! Data written to: kv/apisix/jwt-auth/keys/rsa +Success! Data written to: kv/apisix/consumer/jim/jwt-auth -=== TEST 9: create consumer for RS256 algorithm with keypair fetched from vault +=== TEST 8: create consumer for RS256 algorithm with keypair fetched from vault --- config location /t { content_by_lua_block { @@ -259,7 +250,7 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa local code, body = t('/apisix/admin/consumers', ngx.HTTP_PUT, [[{ - "username": "jack", + "username": "jim", "plugins": { "jwt-auth": { "key": "rsa", @@ -270,7 +261,9 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa }]] ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.say(body) } } @@ -279,7 +272,7 @@ passed -=== TEST 10: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { @@ -297,7 +290,9 @@ passed local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.print(res) } } @@ -306,15 +301,15 @@ successfully invoked secure endpoint -=== TEST 11: store rsa private key into vault from local filesystem +=== TEST 10: store rsa private key into vault from local filesystem --- exec -VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/jwt-auth/keys/rsa1 private_key=@t/certs/private.pem +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/john/jwt-auth private_key=@t/certs/private.pem --- response_body -Success! Data written to: kv/apisix/jwt-auth/keys/rsa1 +Success! Data written to: kv/apisix/consumer/john/jwt-auth -=== TEST 12: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema +=== TEST 11: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema --- config location /t { content_by_lua_block { @@ -322,7 +317,7 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa1 local code, body = t('/apisix/admin/consumers', ngx.HTTP_PUT, [[{ - "username": "jack", + "username": "john", "plugins": { "jwt-auth": { "key": "rsa1", @@ -334,7 +329,9 @@ Success! Data written to: kv/apisix/jwt-auth/keys/rsa1 }]] ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.say(body) } } @@ -343,7 +340,7 @@ passed -=== TEST 13: sign a jwt with with rsa keypair and access /secure-endpoint +=== TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint --- config location /t { content_by_lua_block { @@ -361,7 +358,9 @@ passed local code, _, res = t('/secure-endpoint?jwt=' .. sign, ngx.HTTP_GET ) - ngx.status = code + if code >= 300 then + ngx.status = code + end ngx.print(res) } } From cac28d155e677b42c74b2e873c4e125f65f8fe5f Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Fri, 10 Dec 2021 17:45:24 +0530 Subject: [PATCH 21/25] fix doc broken link --- docs/en/latest/plugins/jwt-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index db282c31b540..a4664fcc18fc 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -57,7 +57,7 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio | base64_secret | boolean | optional | false | | whether secret is base64 encoded | | vault | object | optional | | | whether vault to be used for secret (secret for HS256/HS512 or public_key and private_key for RS256) storage and retrieval. The plugin by default uses the vault path as `kv/apisix/consumer//jwt-auth` for secret retrieval. | -**Note**: To enable vault integration, first visit the [config.yaml](https://github.com/apache/apisix/blob/master/conf/config.yaml) update it with your vault server configuration, host address and access token. You can take a look of what APISIX expects from the config.yaml at [default-config.yaml](https://github.com/apache/apisix/blob/master/conf/default-config.yaml) under the vault attributes. +**Note**: To enable vault integration, first visit the [config.yaml](https://github.com/apache/apisix/blob/master/conf/config.yaml) update it with your vault server configuration, host address and access token. You can take a look of what APISIX expects from the config.yaml at [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) under the vault attributes. ## API From f78cf890db0637e93d33840ba53b5c6fa05dc1a7 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Sun, 12 Dec 2021 22:26:38 +0530 Subject: [PATCH 22/25] comment out vault config in yaml and update tests accordingly --- conf/config-default.yaml | 16 +++++++------- t/plugin/jwt-auth-vault.t | 45 +++++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 23baf2b57916..b33d2c5e5d2a 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -284,15 +284,15 @@ etcd: # HashiCorp Vault storage backend for sensitive data retrieval. The config shows an example of what APISIX expects if you # wish to integrate Vault for secret (sensetive string, public private keys etc.) retrieval. APISIX communicates with Vault # server HTTP APIs. By default, APISIX doesn't need this configuration. -vault: - host: "http://0.0.0.0:8200" # The host address where the vault server is running. - timeout: 10 # request timeout 30 seconds - prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored - # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforcement of - # policies, generate limited scoped tokens and tightly control the data that can be accessed - # from APISIX. +# vault: +# host: "http://0.0.0.0:8200" # The host address where the vault server is running. +# timeout: 10 # request timeout 30 seconds +# token: root # Authentication token to access Vault HTTP APIs +# prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored + # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforcement of + # policies, generate limited scoped tokens and tightly control the data that can be accessed + # from APISIX. - token: root # Authentication token to access Vault HTTP APIs #discovery: # service discovery center # dns: diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index e2b1c3f7c1a7..4275f48ffb0a 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -101,23 +101,8 @@ ok "vault":{} } } - }]], - [[{ - "node": { - "value": { - "username": "jack", - "plugins": { - "jwt-auth": { - "key": "key-hs256", - "algorithm": "HS256", - "vault":{} - } - } - } - }, - "action": "set" }]] - ) + ) if code >= 300 then ngx.status = code @@ -163,6 +148,13 @@ passed === TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault +--- yaml_config +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +#END --- config location /t { content_by_lua_block { @@ -206,6 +198,13 @@ Success! Data written to: kv/apisix/consumer/jack/jwt-auth === TEST 6: sign a HS256 jwt and access/verify /secure-endpoint +--- yaml_config +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +#END --- config location /t { content_by_lua_block { @@ -273,6 +272,13 @@ passed === TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint +--- yaml_config +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +#END --- config location /t { content_by_lua_block { @@ -341,6 +347,13 @@ passed === TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint +--- yaml_config +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +#END --- config location /t { content_by_lua_block { From 2d446549be9de5c9f8dbea5e13234d47c3428cae Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Mon, 13 Dec 2021 09:32:39 +0530 Subject: [PATCH 23/25] change yaml_config to extra_yaml_config --- t/plugin/jwt-auth-vault.t | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 4275f48ffb0a..8477b9c51d12 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -148,7 +148,7 @@ passed === TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault ---- yaml_config +--- extra_yaml_config vault: host: "http://0.0.0.0:8200" timeout: 10 @@ -198,7 +198,7 @@ Success! Data written to: kv/apisix/consumer/jack/jwt-auth === TEST 6: sign a HS256 jwt and access/verify /secure-endpoint ---- yaml_config +--- extra_yaml_config vault: host: "http://0.0.0.0:8200" timeout: 10 @@ -272,7 +272,7 @@ passed === TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint ---- yaml_config +--- extra_yaml_config vault: host: "http://0.0.0.0:8200" timeout: 10 @@ -347,7 +347,7 @@ passed === TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint ---- yaml_config +--- extra_yaml_config vault: host: "http://0.0.0.0:8200" timeout: 10 From 66ee305e0a8ce59e00d143ae5541e4b8a40a0c42 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Mon, 13 Dec 2021 11:31:10 +0530 Subject: [PATCH 24/25] single extra yaml config --- docs/en/latest/plugins/jwt-auth.md | 2 +- t/plugin/jwt-auth-vault.t | 38 ++++++++---------------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index a4664fcc18fc..a5bad8bb84ac 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -142,7 +142,7 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 Here the plugin looks up for key `secret` inside vault path (`/consumer/jack/jwt-auth`) for consumer username `jack` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin logs error and fails to perform jwt authentication. -1. RS256 rsa keypairs, both public and private keys are stored into vault. +2. RS256 rsa keypairs, both public and private keys are stored into vault. ```shell curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 8477b9c51d12..8556e69edf16 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -39,6 +39,16 @@ _EOC_ $block->set_value("http_config", $http_config); + my $vault_config = $block->extra_yaml_config // <<_EOC_; +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +_EOC_ + + $block->set_value("extra_yaml_config", $vault_config); + if (!$block->request) { $block->set_value("request", "GET /t"); } @@ -148,13 +158,6 @@ passed === TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault ---- extra_yaml_config -vault: - host: "http://0.0.0.0:8200" - timeout: 10 - prefix: kv/apisix - token: root -#END --- config location /t { content_by_lua_block { @@ -198,13 +201,6 @@ Success! Data written to: kv/apisix/consumer/jack/jwt-auth === TEST 6: sign a HS256 jwt and access/verify /secure-endpoint ---- extra_yaml_config -vault: - host: "http://0.0.0.0:8200" - timeout: 10 - prefix: kv/apisix - token: root -#END --- config location /t { content_by_lua_block { @@ -272,13 +268,6 @@ passed === TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint ---- extra_yaml_config -vault: - host: "http://0.0.0.0:8200" - timeout: 10 - prefix: kv/apisix - token: root -#END --- config location /t { content_by_lua_block { @@ -347,13 +336,6 @@ passed === TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint ---- extra_yaml_config -vault: - host: "http://0.0.0.0:8200" - timeout: 10 - prefix: kv/apisix - token: root -#END --- config location /t { content_by_lua_block { From a56ed8ed5ac2d0043a6c72c49ea85c8ac94bd529 Mon Sep 17 00:00:00 2001 From: Bisakh Mondal Date: Tue, 14 Dec 2021 09:40:29 +0530 Subject: [PATCH 25/25] suggestion --- t/plugin/jwt-auth-vault.t | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t index 8556e69edf16..c7d9e421c835 100644 --- a/t/plugin/jwt-auth-vault.t +++ b/t/plugin/jwt-auth-vault.t @@ -184,8 +184,7 @@ passed --- response_body failed to sign jwt --- error_code: 503 ---- error_log: true ---- grep_error_log eval +--- error_log eval qr/failed to sign jwt, err: secret could not found in vault/ --- grep_error_log_out failed to sign jwt, err: secret could not found in vault