Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support global data encryption of secret information #8403

Merged
merged 17 commits into from
Nov 30, 2022
9 changes: 9 additions & 0 deletions apisix/admin/consumers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ local function check_conf(username, conf)
if plugin_obj.type == 'auth' then
count_auth_plugin = count_auth_plugin + 1
end

plugin.encrypt_conf(name, conf, core.schema.TYPE_CONSUMER)
end

if count_auth_plugin == 0 then
Expand Down Expand Up @@ -119,6 +121,13 @@ function _M.get(consumer_name)
end

utils.fix_count(res.body, consumer_name)

-- decrypt the conf
if res.body then
utils.decrypt_conf(plugin.decrypt_conf, res.body, core.schema.TYPE_CONSUMER)
end


return res.status, res.body
end

Expand Down
11 changes: 11 additions & 0 deletions apisix/admin/routes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ local core = require("apisix.core")
local apisix_upstream = require("apisix.upstream")
local schema_plugin = require("apisix.admin.plugins").check_schema
local utils = require("apisix.admin.utils")
local plugin = require("apisix.plugin")
local pairs = pairs
local tostring = tostring
local type = type
local loadstring = loadstring
Expand Down Expand Up @@ -131,6 +133,9 @@ local function check_conf(id, conf, need_id)
if not ok then
return nil, {error_msg = err}
end
for name, conf in pairs(conf.plugins) do
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
plugin.encrypt_conf(name, conf)
end
end

if conf.vars then
Expand Down Expand Up @@ -204,6 +209,12 @@ function _M.get(id)
end

utils.fix_count(res.body, id)

-- decrypt the conf
if res.body then
utils.decrypt_conf(plugin.decrypt_conf, res.body)
end

return res.status, res.body
end

Expand Down
27 changes: 27 additions & 0 deletions apisix/admin/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
local core = require("apisix.core")
local ngx_time = ngx.time
local tonumber = tonumber
local ipairs = ipairs
local pairs = pairs


local _M = {}
Expand Down Expand Up @@ -78,4 +80,29 @@ function _M.fix_count(body, id)
end


function _M.decrypt_conf(decrypt_func, body, schema_type)
-- list
if body.list and #body.list > 0 then
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
for _, route in ipairs(body.list) do
if route.value and route.value.plugins
and core.table.nkeys(route.value.plugins) > 0 then
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
for name, conf in pairs(route.value.plugins) do
decrypt_func(name, conf, schema_type)
end
end
end
return
end

-- node
local plugins = body.node and body.node.value
and body.node.value.plugins

if plugins then
for name, conf in pairs(plugins) do
decrypt_func(name, conf, schema_type)
end
end
end

return _M
1 change: 1 addition & 0 deletions apisix/consumer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ local lrucache = core.lrucache.new({
ttl = 300, count = 512
})


local function plugin_consumer()
local plugins = {}

Expand Down
70 changes: 69 additions & 1 deletion apisix/plugin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ local config_util = require("apisix.core.config_util")
local enable_debug = require("apisix.debug").enable_debug
local wasm = require("apisix.wasm")
local expr = require("resty.expr.v1")
local apisix_ssl = require("apisix.ssl")
local ngx = ngx
local crc32 = ngx.crc32_short
local ngx_exit = ngx.exit
Expand Down Expand Up @@ -849,6 +850,65 @@ check_plugin_metadata = function(item)
end


local function check_enable_and_get_plugin_schema(name, schema_type)
local plugin_schema = local_plugins_hash and local_plugins_hash[name]
local schema
if schema_type == core.schema.TYPE_CONSUMER then
schema = plugin_schema.consumer_schema
else
schema = plugin_schema.schema
end

return schema
end


local function decrypt_conf(name, conf, schema_type)
local enable = core.table.try_read_attr(local_conf, "apisix", "data_encryption", "enable")
if not enable then
return conf
end

local schema = check_enable_and_get_plugin_schema(name, schema_type)
if not schema then
return
end

for key, props in pairs(schema.properties) do
if props.type == "string" and props.encrypted and conf[key] then
local encrypted, err = apisix_ssl.aes_decrypt_pkey(conf[key], "data_encrypt")
if not encrypted then
core.log.warn("failed to decrypt the conf of plugin [", name,
"] key [", key, "], err: ", err)
else
conf[key] = encrypted
end
end
end
end
_M.decrypt_conf = decrypt_conf


local function encrypt_conf(name, conf, schema_type)
local enable = core.table.try_read_attr(local_conf, "apisix", "data_encryption", "enable")
if not enable then
return conf
end

local schema = check_enable_and_get_plugin_schema(name, schema_type)
if not schema then
return
end

for key, props in pairs(schema.properties) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we consider the case of configuration nesting here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that since this PR does not include this case, we can optimize this point in the next PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to notice this point at doc ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed and will finish it soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit complicated, like 'anyof', 'oneof' need to be considered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyof or oneof only restricts the existence of items, if an item has encrypted = true but does not exist in conf, it will not be encrypted.

if props.type == "string" and props.encrypted and conf[key] then
local encrypted = apisix_ssl.aes_encrypt_pkey(conf[key], "data_encrypt")
conf[key] = encrypted
end
end
end
_M.encrypt_conf = encrypt_conf


local function check_schema(plugins_conf, schema_type, skip_disabled_plugin)
for name, plugin_conf in pairs(plugins_conf) do
Expand Down Expand Up @@ -901,7 +961,15 @@ _M.stream_check_schema = stream_check_schema

function _M.plugin_checker(item, schema_type)
if item.plugins then
return check_schema(item.plugins, schema_type, true)
local ok, err = check_schema(item.plugins, schema_type, true)

if ok then
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
-- decrypt conf
for name, conf in pairs(item.plugins) do
decrypt_conf(name, conf, schema_type)
end
end
return ok, err
end

return true
Expand Down
3 changes: 1 addition & 2 deletions apisix/plugins/basic-auth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ local core = require("apisix.core")
local ngx = ngx
local ngx_re = require("ngx.re")
local consumer = require("apisix.consumer")

local lrucache = core.lrucache.new({
ttl = 300, count = 512
})
Expand All @@ -39,7 +38,7 @@ local consumer_schema = {
title = "work with consumer object",
properties = {
username = { type = "string" },
password = { type = "string" },
password = { type = "string", encrypted = true },
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
},
required = {"username", "password"},
}
Expand Down
2 changes: 1 addition & 1 deletion apisix/plugins/clickhouse-logger.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ local schema = {
endpoint_addr = core.schema.uri_def,
endpoint_addrs = {items = core.schema.uri_def, type = "array", minItems = 1},
user = {type = "string", default = ""},
password = {type = "string", default = ""},
password = {type = "string", default = "", encrypted = true},
database = {type = "string", default = ""},
logtable = {type = "string", default = ""},
timeout = {type = "integer", minimum = 1, default = 3},
Expand Down
2 changes: 1 addition & 1 deletion apisix/plugins/key-auth.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ local schema = {
local consumer_schema = {
type = "object",
properties = {
key = {type = "string"},
key = { type = "string", encrypted = true },
},
required = {"key"},
}
Expand Down
115 changes: 82 additions & 33 deletions apisix/ssl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,52 +56,96 @@ function _M.server_name()
end


local _aes_128_cbc_with_iv_tbl
local function get_aes_128_cbc_with_iv()
if _aes_128_cbc_with_iv_tbl == nil then
_aes_128_cbc_with_iv_tbl = core.table.new(2, 0)
local local_conf = core.config.local_conf()
local ivs = core.table.try_read_attr(local_conf, "apisix", "ssl", "key_encrypt_salt")
local type_ivs = type(ivs)
local function init_iv_tbl(ivs)
local _aes_128_cbc_with_iv_tbl = core.table.new(2, 0)
local type_ivs = type(ivs)

if type_ivs == "table" then
for _, iv in ipairs(ivs) do
local aes_with_iv = assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv = iv}))
core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)
end
elseif type_ivs == "string" then
local aes_with_iv = assert(aes:new(ivs, nil, aes.cipher(128, "cbc"), {iv = ivs}))
if type_ivs == "table" then
for _, iv in ipairs(ivs) do
local aes_with_iv = assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv = iv}))
core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)
end
elseif type_ivs == "string" then
local aes_with_iv = assert(aes:new(ivs, nil, aes.cipher(128, "cbc"), {iv = ivs}))
core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv)
end

return _aes_128_cbc_with_iv_tbl
end


function _M.aes_encrypt_pkey(origin)
local aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv()
local aes_128_cbc_with_iv = aes_128_cbc_with_iv_tbl[1]
if aes_128_cbc_with_iv ~= nil and core.string.has_prefix(origin, "---") then
local encrypted = aes_128_cbc_with_iv:encrypt(origin)
if encrypted == nil then
core.log.error("failed to encrypt key[", origin, "] ")
return origin
end
local _aes_128_cbc_with_iv_tbl_ssl
local function get_aes_128_cbc_with_iv_ssl(local_conf)
if _aes_128_cbc_with_iv_tbl_ssl == nil then
local ivs = core.table.try_read_attr(local_conf, "apisix", "ssl", "key_encrypt_salt")
_aes_128_cbc_with_iv_tbl_ssl = init_iv_tbl(ivs)
end

return _aes_128_cbc_with_iv_tbl_ssl
end

return ngx_encode_base64(encrypted)

local _aes_128_cbc_with_iv_tbl_gde
local function get_aes_128_cbc_with_iv_gde(local_conf)
if _aes_128_cbc_with_iv_tbl_gde == nil then
local ivs = core.table.try_read_attr(local_conf, "apisix", "data_encryption", "keyring")
_aes_128_cbc_with_iv_tbl_gde = init_iv_tbl(ivs)
end

return origin
return _aes_128_cbc_with_iv_tbl_gde
end


local function aes_decrypt_pkey(origin)
if core.string.has_prefix(origin, "---") then

local function encrypt(aes_128_cbc_with_iv, origin)
local encrypted = aes_128_cbc_with_iv:encrypt(origin)
if encrypted == nil then
core.log.error("failed to encrypt key[", origin, "] ")
return origin
end

local aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv()
return ngx_encode_base64(encrypted)
end

function _M.aes_encrypt_pkey(origin, field)
local local_conf = core.config.local_conf()

if not field then
-- default used by ssl
local aes_128_cbc_with_iv_tbl_ssl = get_aes_128_cbc_with_iv_ssl(local_conf)
local aes_128_cbc_with_iv_ssl = aes_128_cbc_with_iv_tbl_ssl[1]
if aes_128_cbc_with_iv_ssl ~= nil and core.string.has_prefix(origin, "---") then
return encrypt(aes_128_cbc_with_iv_ssl, origin)
end
else
if field == "data_encrypt" then
local aes_128_cbc_with_iv_tbl_gde = get_aes_128_cbc_with_iv_gde(local_conf)
local aes_128_cbc_with_iv_gde = aes_128_cbc_with_iv_tbl_gde[1]
if aes_128_cbc_with_iv_gde ~= nil then
return encrypt(aes_128_cbc_with_iv_gde, origin)
end
end
end

return origin
end


local function aes_decrypt_pkey(origin, field)
local local_conf = core.config.local_conf()
local aes_128_cbc_with_iv_tbl

if not field then
if core.string.has_prefix(origin, "---") then
return origin
end
aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv_ssl(local_conf)
else
if field == "data_encrypt" then
aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv_gde(local_conf)
end
end

if #aes_128_cbc_with_iv_tbl == 0 then
return origin
end
Expand All @@ -119,10 +163,9 @@ local function aes_decrypt_pkey(origin)
end
end

core.log.error("decrypt ssl key failed")

return nil
return nil, "decrypt ssl key failed"
end
_M.aes_decrypt_pkey = aes_decrypt_pkey


local function validate(cert, key)
Expand All @@ -136,8 +179,10 @@ local function validate(cert, key)
return true
end

key = aes_decrypt_pkey(key)
local err
key, err = aes_decrypt_pkey(key)
if not key then
core.log.error(err)
return nil, "failed to decrypt previous encrypted key"
end

Expand Down Expand Up @@ -173,7 +218,11 @@ end
local function parse_pem_priv_key(sni, pkey)
core.log.debug("parsing priv key for sni: ", sni)

local parsed, err = ngx_ssl.parse_pem_priv_key(aes_decrypt_pkey(pkey))
local key, err = aes_decrypt_pkey(pkey)
if not key then
core.log.error(err)
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
end
local parsed, err = ngx_ssl.parse_pem_priv_key(key)
return parsed, err
end

Expand Down
Loading