diff --git a/apisix/admin/ssl.lua b/apisix/admin/ssl.lua index 898d9c1a988f..ccf3047a2aea 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 83dc2dcf88c1..bfe7160f066c 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 e8d2b75c0bab..a090e549153d 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -499,6 +499,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"}}, @@ -508,6 +514,7 @@ _M.ssl = { } + _M.proto = { type = "object", properties = { diff --git a/conf/config.yaml b/conf/config.yaml index bb147bbee198..9f794f7b72c2 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-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA" + 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 834446e4c06b..dc245c3bb7b7 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 d68a813253c8..86724e04f2ce 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.