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/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ local core = require("apisix.core")
local route = require("apisix.utils.router")
local plugin = require("apisix.plugin")
local v3_adapter = require("apisix.admin.v3_adapter")
local utils = require("apisix.admin.utils")
local ngx = ngx
local get_method = ngx.req.get_method
local ngx_time = ngx.time
Expand Down Expand Up @@ -188,6 +189,14 @@ local function run()
local code, data = resource[method](seg_id, req_body, seg_sub_path,
uri_args)
if code then
if method == "get" and plugin.enable_data_encryption then
if seg_res == "consumers" then
utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_CONSUMER)
else
utils.decrypt_params(plugin.decrypt_conf, data)
end
end

if v3_adapter.enable_v3() then
core.response.set_header("X-API-VERSION", "v3")
else
Expand Down
10 changes: 9 additions & 1 deletion apisix/admin/plugins.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,20 @@ local table_sort = table.sort
local table_insert = table.insert
local get_uri_args = ngx.req.get_uri_args
local plugin_get_all = require("apisix.plugin").get_all
local encrypt_conf = require("apisix.plugin").encrypt_conf
local pairs = pairs

local _M = {}


function _M.check_schema(plugins_conf, schema_type)
return check_schema(plugins_conf, schema_type, false)
local ok, err = check_schema(plugins_conf, schema_type, false)
if ok then
for name, conf in pairs(plugins_conf) do
encrypt_conf(name, conf, schema_type)
end
end
return ok, err
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_params(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
76 changes: 75 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,71 @@ check_plugin_metadata = function(item)
end


local enable_data_encryption
local function enable_gde()
if enable_data_encryption == nil then
enable_data_encryption =
core.table.try_read_attr(local_conf, "apisix", "data_encryption", "enable")
_M.enable_data_encryption = enable_data_encryption
spacewander marked this conversation as resolved.
Show resolved Hide resolved
end

return enable_data_encryption
end


local function get_plugin_schema(name, schema_type)
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
if not enable_gde() then
return false
tzssangglass marked this conversation as resolved.
Show resolved Hide resolved
end

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 schema = 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 schema = 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 +967,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 and enable_gde() then
-- 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
7 changes: 7 additions & 0 deletions conf/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ apisix:
# ip: 127.0.0.1
# port: 9090
disable_sync_configuration_during_start: false # safe exit. Remove this once the feature is stable
data_encryption: # add `encrypted = true` in plugin schema to enable encryption
enable: false # if not set, the default value is `false`.
keyring:
- qeddd145sfvddff3 # If not set, will save origin value into etcd.
# If set this, the keyring should be an array whose elements are string, and the size is also 16, and it will encrypt fields with AES-128-CBC
# !!! So do not change it after encryption, it can't decrypt the fields have be saved if you change !!
# Only use the first key to encrypt, and decrypt in the order of the array.

nginx_config: # config for render the template to generate nginx.conf
#user: root # specifies the execution user of the worker process.
Expand Down
Loading