You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/06/13 08:10:35 UTC
[incubator-apisix] branch master updated: feature: ssl enhance
(#1678)
This is an automated email from the ASF dual-hosted git repository.
membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 56aeb4a feature: ssl enhance (#1678)
56aeb4a is described below
commit 56aeb4a6a94c5f6f53830ed10948f96eff834642
Author: nic-chen <33...@users.noreply.github.com>
AuthorDate: Sat Jun 13 16:10:27 2020 +0800
feature: ssl enhance (#1678)
support enable or disable ssl by patch method
support encrypted storage of the SSL private key in etcd
support multi snis
Fix #1668
---
apisix/admin/ssl.lua | 95 ++++++++-
apisix/http/router/radixtree_sni.lua | 71 ++++++-
apisix/schema_def.lua | 7 +
conf/config.yaml | 3 +
t/lib/test_admin.lua | 27 ++-
t/router/radixtree-sni.t | 376 +++++++++++++++++++++++++++++++++++
6 files changed, 564 insertions(+), 15 deletions(-)
diff --git a/apisix/admin/ssl.lua b/apisix/admin/ssl.lua
index 898d9c1..ccf3047 100644
--- a/apisix/admin/ssl.lua
+++ b/apisix/admin/ssl.lua
@@ -14,10 +14,14 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local core = require("apisix.core")
-local schema_plugin = require("apisix.admin.plugins").check_schema
-local tostring = tostring
-
+local core = require("apisix.core")
+local schema_plugin = require("apisix.admin.plugins").check_schema
+local tostring = tostring
+local aes = require "resty.aes"
+local ngx_encode_base64 = ngx.encode_base64
+local str_find = string.find
+local type = type
+local assert = assert
local _M = {
version = 0.1,
@@ -94,12 +98,39 @@ local function check_conf(id, conf, need_id)
end
+local function aes_encrypt(origin)
+ local local_conf = core.config.local_conf()
+ local iv
+ if local_conf and local_conf.apisix
+ and local_conf.apisix.ssl.key_encrypt_salt then
+ iv = local_conf.apisix.ssl.key_encrypt_salt
+ end
+ local aes_128_cbc_with_iv = (type(iv)=="string" and #iv == 16) and
+ assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv=iv})) or nil
+
+ if aes_128_cbc_with_iv ~= nil and str_find(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
+
+ return ngx_encode_base64(encrypted)
+ end
+
+ return origin
+end
+
+
function _M.put(id, conf)
local id, err = check_conf(id, conf, true)
if not id then
return 400, err
end
+ -- encrypt private key
+ conf.key = aes_encrypt(conf.key)
+
local key = "/ssl/" .. id
local res, err = core.etcd.set(key, conf)
if not res then
@@ -138,6 +169,9 @@ function _M.post(id, conf)
return 400, err
end
+ -- encrypt private key
+ conf.key = aes_encrypt(conf.key)
+
local key = "/ssl"
-- core.log.info("key: ", key)
local res, err = core.etcd.push("/ssl", conf)
@@ -167,4 +201,57 @@ function _M.delete(id)
end
+function _M.patch(id, conf)
+ if not id then
+ return 400, {error_msg = "missing route id"}
+ end
+
+ if not conf then
+ return 400, {error_msg = "missing new configuration"}
+ end
+
+ if type(conf) ~= "table" then
+ return 400, {error_msg = "invalid configuration"}
+ end
+
+ local key = "/ssl"
+ if id then
+ key = key .. "/" .. id
+ end
+
+ local res_old, err = core.etcd.get(key)
+ if not res_old then
+ core.log.error("failed to get ssl [", key, "] in etcd: ", err)
+ return 500, {error_msg = err}
+ end
+
+ if res_old.status ~= 200 then
+ return res_old.status, res_old.body
+ end
+ core.log.info("key: ", key, " old value: ",
+ core.json.delay_encode(res_old, true))
+
+
+ local node_value = res_old.body.node.value
+
+ node_value = core.table.merge(node_value, conf);
+
+ core.log.info("new ssl conf: ", core.json.delay_encode(node_value, true))
+
+ local id, err = check_conf(id, node_value, true)
+ if not id then
+ return 400, err
+ end
+
+ -- TODO: this is not safe, we need to use compare-set
+ local res, err = core.etcd.set(key, node_value)
+ if not res then
+ core.log.error("failed to set new ssl[", key, "] to etcd: ", err)
+ return 500, {error_msg = err}
+ end
+
+ return res.status, res.body
+end
+
+
return _M
diff --git a/apisix/http/router/radixtree_sni.lua b/apisix/http/router/radixtree_sni.lua
index 83dc2dc..bfe7160 100644
--- a/apisix/http/router/radixtree_sni.lua
+++ b/apisix/http/router/radixtree_sni.lua
@@ -22,6 +22,9 @@ local ipairs = ipairs
local type = type
local error = error
local str_find = string.find
+local aes = require "resty.aes"
+local assert = assert
+local ngx_decode_base64 = ngx.decode_base64
local ssl_certificates
local radixtree_router
local radixtree_router_ver
@@ -39,9 +42,45 @@ local function create_router(ssl_items)
local route_items = core.table.new(#ssl_items, 0)
local idx = 0
+ local local_conf = core.config.local_conf()
+ local iv
+ if local_conf and local_conf.apisix
+ and local_conf.apisix.ssl
+ and local_conf.apisix.ssl.key_encrypt_salt then
+ iv = local_conf.apisix.ssl.key_encrypt_salt
+ end
+ local aes_128_cbc_with_iv = (type(iv)=="string" and #iv == 16) and
+ assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv=iv})) or nil
+
for _, ssl in ipairs(ssl_items) do
- if type(ssl) == "table" then
- local sni = ssl.value.sni:reverse()
+ if type(ssl) == "table" and
+ ssl.value ~= nil and
+ (ssl.value.status == nil or ssl.value.status == 1) then -- compatible with old version
+
+ local j = 0
+ local sni
+ if type(ssl.value.snis) == "table" and #ssl.value.snis > 0 then
+ sni = core.table.new(0, #ssl.value.snis)
+ for _, s in ipairs(ssl.value.snis) do
+ j = j + 1
+ sni[j] = s:reverse()
+ end
+ else
+ sni = ssl.value.sni:reverse()
+ end
+
+ -- decrypt private key
+ if aes_128_cbc_with_iv ~= nil and
+ not str_find(ssl.value.key, "---") then
+ local decrypted = aes_128_cbc_with_iv:decrypt(ngx_decode_base64(ssl.value.key))
+ if decrypted == nil then
+ core.log.error("decrypt ssl key failed. key[", ssl.value.key, "] ")
+ else
+ ssl.value.key = decrypted
+ end
+ end
+
+ local
idx = idx + 1
route_items[idx] = {
paths = sni,
@@ -116,6 +155,7 @@ function _M.match_and_set(api_ctx)
end
core.log.debug("sni: ", sni)
+
local sni_rev = sni:reverse()
local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx)
if not ok then
@@ -123,14 +163,29 @@ function _M.match_and_set(api_ctx)
return false
end
- if str_find(sni_rev, ".", #api_ctx.matched_sni, true) then
- core.log.warn("not found any valid sni configuration, matched sni: ",
- api_ctx.matched_sni:reverse(), " current sni: ", sni)
- return false
+
+ if type(api_ctx.matched_sni) == "table" then
+ local matched = false
+ for _, msni in ipairs(api_ctx.matched_sni) do
+ if sni_rev == msni or not str_find(sni_rev, ".", #msni, true) then
+ matched = true
+ end
+ end
+ if not matched then
+ core.log.warn("not found any valid sni configuration, matched sni: ",
+ core.json.delay_encode(api_ctx.matched_sni, true), " current sni: ", sni)
+ return false
+ end
+ else
+ if str_find(sni_rev, ".", #api_ctx.matched_sni, true) then
+ core.log.warn("not found any valid sni configuration, matched sni: ",
+ api_ctx.matched_sni:reverse(), " current sni: ", sni)
+ return false
+ end
end
local matched_ssl = api_ctx.matched_ssl
- core.log.info("debug: ", core.json.delay_encode(matched_ssl, true))
+ core.log.info("debug - matched: ", core.json.delay_encode(matched_ssl, true))
ok, err = set_pem_ssl_key(matched_ssl.value.cert, matched_ssl.value.key)
if not ok then
return false, err
@@ -144,7 +199,7 @@ function _M.init_worker()
local err
ssl_certificates, err = core.config.new("/ssl", {
automatic = true,
- item_schema = core.schema.ssl
+ item_schema = core.schema.ssl,
})
if not ssl_certificates then
error("failed to create etcd instance for fetching ssl certificates: "
diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua
index 0d59e77..ab75e62 100644
--- a/apisix/schema_def.lua
+++ b/apisix/schema_def.lua
@@ -502,6 +502,12 @@ _M.ssl = {
type = "integer",
minimum = 1588262400, -- 2020/5/1 0:0:0
},
+ status = {
+ description = "ssl status, 1 to enable, 0 to disable",
+ type = "integer",
+ enum = {1, 0},
+ default = 1
+ }
},
oneOf = {
{required = {"sni", "key", "cert"}},
@@ -511,6 +517,7 @@ _M.ssl = {
}
+
_M.proto = {
type = "object",
properties = {
diff --git a/conf/config.yaml b/conf/config.yaml
index 092bcc9..6137a15 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -93,6 +93,9 @@ apisix:
listen_port: 9443
ssl_protocols: "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"
ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES2 [...]
+ key_encrypt_salt: "edd1c9f0985e76a2" # If not set, will save origin ssl key into etcd.
+ # If set this, must be a string of length 16. And it will encrypt ssl key with AES-128-CBC
+ # !!! So do not change it after saving your ssl, it can't decrypt the ssl keys have be saved if you change !!
# discovery: eureka # service discovery center
nginx_config: # config for render the template to genarate nginx.conf
error_log: "logs/error.log"
diff --git a/t/lib/test_admin.lua b/t/lib/test_admin.lua
index 834446e..dc245c3 100644
--- a/t/lib/test_admin.lua
+++ b/t/lib/test_admin.lua
@@ -14,9 +14,12 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-local http = require("resty.http")
-local json = require("cjson.safe")
-local dir_names = {}
+local http = require("resty.http")
+local json = require("cjson.safe")
+local aes = require "resty.aes"
+local ngx_encode_base64 = ngx.encode_base64
+local str_find = string.find
+local dir_names = {}
local _M = {}
@@ -210,4 +213,22 @@ function _M.req_self_with_http(uri, method, body, headers)
end
+function _M.aes_encrypt(origin)
+ local iv = "1234567890123456"
+ local aes_128_cbc_with_iv = assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv=iv}))
+
+ if aes_128_cbc_with_iv ~= nil and str_find(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
+
+ return ngx_encode_base64(encrypted)
+ end
+
+ return origin
+end
+
+
return _M
diff --git a/t/router/radixtree-sni.t b/t/router/radixtree-sni.t
index d68a813..86724e0 100644
--- a/t/router/radixtree-sni.t
+++ b/t/router/radixtree-sni.t
@@ -565,3 +565,379 @@ not found any valid sni configuration, matched sni: *.test2.com current sni: aa.
--- no_error_log
[error]
[alert]
+
+
+
+=== TEST 12: disable ssl(sni: *.test2.com)
+--- config
+location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local data = {status = 0}
+
+ local code, body = t.test('/apisix/admin/ssl/1',
+ ngx.HTTP_PATCH,
+ core.json.encode(data),
+ [[{
+ "node": {
+ "value": {
+ "status": 0
+ },
+ "key": "/apisix/ssl/1"
+ },
+ "action": "set"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+}
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: client request: www.test2.com -- failed by disable
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+
+ do
+ local sock = ngx.socket.tcp()
+
+ sock:settimeout(2000)
+
+ local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local sess, err = sock:sslhandshake(nil, "www.test2.com", true)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ ngx.say("ssl handshake: ", type(sess))
+ end -- do
+ -- collectgarbage()
+ }
+}
+--- request
+GET /t
+--- response_body
+connected: 1
+failed to do SSL handshake: certificate host mismatch
+--- error_log
+lua ssl server name: "www.test2.com"
+--- no_error_log
+[error]
+[alert]
+
+
+
+=== TEST 14: enable ssl(sni: *.test2.com)
+--- config
+location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local data = {status = 1}
+
+ local code, body = t.test('/apisix/admin/ssl/1',
+ ngx.HTTP_PATCH,
+ core.json.encode(data),
+ [[{
+ "node": {
+ "value": {
+ "status": 1
+ },
+ "key": "/apisix/ssl/1"
+ },
+ "action": "set"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+}
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 15: client request: www.test2.com again
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+
+ do
+ local sock = ngx.socket.tcp()
+
+ sock:settimeout(2000)
+
+ local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local sess, err = sock:sslhandshake(nil, "www.test2.com", true)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ ngx.say("ssl handshake: ", type(sess))
+ end -- do
+ -- collectgarbage()
+ }
+}
+--- request
+GET /t
+--- response_body
+connected: 1
+failed to do SSL handshake: 18: self signed certificate
+--- error_log
+lua ssl server name: "www.test2.com"
+--- no_error_log
+[error]
+[alert]
+
+
+
+=== TEST 16: set ssl(snis: {test2.com, *.test2.com})
+--- config
+location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("conf/cert/test2.crt")
+ local ssl_key = t.read_file("conf/cert/test2.key")
+ local data = {cert = ssl_cert, key = ssl_key, snis = {"test2.com", "*.test2.com"}}
+
+ local code, body = t.test('/apisix/admin/ssl/1',
+ ngx.HTTP_PUT,
+ core.json.encode(data),
+ [[{
+ "node": {
+ "value": {
+ "snis": ["test2.com", "*.test2.com"]
+ },
+ "key": "/apisix/ssl/1"
+ },
+ "action": "set"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+}
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: client request: test2.com
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+
+ do
+ local sock = ngx.socket.tcp()
+
+ sock:settimeout(2000)
+
+ local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local sess, err = sock:sslhandshake(nil, "test2.com", true)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ ngx.say("ssl handshake: ", type(sess))
+ end -- do
+ -- collectgarbage()
+ }
+}
+--- request
+GET /t
+--- response_body
+connected: 1
+failed to do SSL handshake: 18: self signed certificate
+--- error_log
+lua ssl server name: "test2.com"
+--- no_error_log
+[error]
+[alert]
+
+
+
+=== TEST 18: client request: aa.bb.test2.com -- snis un-include
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+
+ do
+ local sock = ngx.socket.tcp()
+
+ sock:settimeout(2000)
+
+ local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local sess, err = sock:sslhandshake(nil, "aa.bb.test2.com", true)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ ngx.say("ssl handshake: ", type(sess))
+ end -- do
+ -- collectgarbage()
+ }
+}
+--- request
+GET /t
+--- response_body
+connected: 1
+failed to do SSL handshake: certificate host mismatch
+--- error_log
+lua ssl server name: "aa.bb.test2.com"
+not found any valid sni configuration, matched sni: ["moc.2tset","moc.2tset.*"] current sni: aa.bb.test2.com
+--- no_error_log
+[error]
+[alert]
+
+
+
+=== TEST 19: set ssl(encrypt ssl key with another iv)
+--- config
+location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+
+ local ssl_cert = t.read_file("conf/cert/test2.crt")
+ local ssl_key = t.aes_encrypt(t.read_file("conf/cert/test2.key"))
+ local data = {cert = ssl_cert, key = ssl_key, snis = {"test2.com", "*.test2.com"}}
+
+ local code, body = t.test('/apisix/admin/ssl/1',
+ ngx.HTTP_PUT,
+ core.json.encode(data),
+ [[{
+ "node": {
+ "value": {
+ "snis": ["test2.com", "*.test2.com"]
+ },
+ "key": "/apisix/ssl/1"
+ },
+ "action": "set"
+ }]]
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+}
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 20: client request: test2.com
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+
+ do
+ local sock = ngx.socket.tcp()
+
+ sock:settimeout(2000)
+
+ local ok, err = sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local sess, err = sock:sslhandshake(nil, "test2.com", true)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+
+ ngx.say("ssl handshake: ", type(sess))
+ end -- do
+ -- collectgarbage()
+ }
+}
+--- request
+GET /t
+--- response_body
+connected: 1
+failed to do SSL handshake: handshake failed
+--- error_log
+decrypt ssl key failed.