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.