diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua index 3f33e33a1d3f..13af90f4abb2 100644 --- a/apisix/cli/file.lua +++ b/apisix/cli/file.lua @@ -133,6 +133,10 @@ local function path_is_multi_type(path, type_val) return true end + if path == "apisix->ssl->key_encrypt_salt" then + return true + end + return false end diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index c81afebfa1a7..e1c1a0920812 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -219,7 +219,25 @@ local config_schema = { } } } - } + }, + key_encrypt_salt = { + anyOf = { + { + type = "array", + minItems = 1, + items = { + type = "string", + minLength = 16, + maxLength = 16 + } + }, + { + type = "string", + minLength = 16, + maxLength = 16 + } + } + }, } }, } diff --git a/apisix/ssl.lua b/apisix/ssl.lua index 7d48f308502e..26fa5c6ef359 100644 --- a/apisix/ssl.lua +++ b/apisix/ssl.lua @@ -22,6 +22,7 @@ local aes = require("resty.aes") local str_lower = string.lower local assert = assert local type = type +local ipairs = ipairs local cert_cache = core.lrucache.new { @@ -55,23 +56,32 @@ function _M.server_name() end -local _aes_128_cbc_with_iv = false +local _aes_128_cbc_with_iv_tbl local function get_aes_128_cbc_with_iv() - if _aes_128_cbc_with_iv == false then + 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 iv = core.table.try_read_attr(local_conf, "apisix", "ssl", "key_encrypt_salt") - if type(iv) =="string" and #iv == 16 then - _aes_128_cbc_with_iv = assert(aes:new(iv, nil, aes.cipher(128, "cbc"), {iv = iv})) - else - _aes_128_cbc_with_iv = nil + local ivs = core.table.try_read_attr(local_conf, "apisix", "ssl", "key_encrypt_salt") + 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})) + core.table.insert(_aes_128_cbc_with_iv_tbl, aes_with_iv) end end - return _aes_128_cbc_with_iv + + return _aes_128_cbc_with_iv_tbl end function _M.aes_encrypt_pkey(origin) - local aes_128_cbc_with_iv = get_aes_128_cbc_with_iv() + 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 @@ -86,32 +96,32 @@ function _M.aes_encrypt_pkey(origin) end -local function decrypt_priv_pkey(iv, key) - local decoded_key = ngx_decode_base64(key) - if not decoded_key then - core.log.error("base64 decode ssl key failed. key[", key, "] ") - return nil +local function aes_decrypt_pkey(origin) + if core.string.has_prefix(origin, "---") then + return origin end - local decrypted = iv:decrypt(decoded_key) - if not decrypted then - core.log.error("decrypt ssl key failed. key[", key, "] ") + local aes_128_cbc_with_iv_tbl = get_aes_128_cbc_with_iv() + if #aes_128_cbc_with_iv_tbl == 0 then + return origin end - return decrypted -end - - -local function aes_decrypt_pkey(origin) - if core.string.has_prefix(origin, "---") then - return origin + local decoded_key = ngx_decode_base64(origin) + if not decoded_key then + core.log.error("base64 decode ssl key failed. key[", origin, "] ") + return nil end - local aes_128_cbc_with_iv = get_aes_128_cbc_with_iv() - if aes_128_cbc_with_iv ~= nil then - return decrypt_priv_pkey(aes_128_cbc_with_iv, origin) + for _, aes_128_cbc_with_iv in ipairs(aes_128_cbc_with_iv_tbl) do + local decrypted = aes_128_cbc_with_iv:decrypt(decoded_key) + if decrypted then + return decrypted + end end - return origin + + core.log.error("decrypt ssl key failed") + + return nil end diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 3e4d06ecc51a..f6b81824e0c5 100755 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -112,9 +112,10 @@ apisix: ssl_session_tickets: false # disable ssl_session_tickets by default for 'ssl_session_tickets' would make Perfect Forward Secrecy useless. # ref: https://github.com/mozilla/server-side-tls/issues/135 - 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 !! + key_encrypt_salt: # If not set, will save origin ssl key into etcd. + - edd1c9f0985e76a2 # If set this, the key_encrypt_salt should be an array whose elements are string, and the size is also 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 !! + # Only use the first key to encrypt, and decrypt in the order of the array. #fallback_sni: "my.default.domain" # If set this, when the client doesn't send SNI during handshake, the fallback SNI will be used instead enable_control: true diff --git a/t/admin/ssl4.t b/t/admin/ssl4.t new file mode 100644 index 000000000000..2c1403407d85 --- /dev/null +++ b/t/admin/ssl4.t @@ -0,0 +1,357 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +log_level('debug'); +no_root_location(); + +add_block_preprocessor( sub{ + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + my $TEST_NGINX_HTML_DIR ||= html_dir(); + + my $config = <<_EOC_; +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.test.com", true) + if not sess then + ngx.say("failed to do SSL handshake: ", err) + return + end + + ngx.say("ssl handshake: ", sess ~= nil) + + local req = "GET /hello HTTP/1.0\\r\\nHost: www.test.com\\r\\nConnection: close\\r\\n\\r\\n" + local bytes, err = sock:send(req) + if not bytes then + ngx.say("failed to send http request: ", err) + return + end + + ngx.say("sent http request: ", bytes, " bytes.") + + while true do + local line, err = sock:receive() + if not line then + break + end + + ngx.say("received: ", line) + end + + local ok, err = sock:close() + ngx.say("close: ", ok, " ", err) + end -- do + -- collectgarbage() + } +} +_EOC_ + + if (!$block->config) { + $block->set_value("config", $config) + } +} + +); + + +run_tests; + +__DATA__ + +=== TEST 1: set ssl(sni: www.test.com), encrypt with the first key_encrypt_salt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: + - edd1c9f0985e76a1 + - edd1c9f0985e76a2 +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "www.test.com"} + + local code, body = t.test('/apisix/admin/ssls/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "value": { + "sni": "www.test.com" + }, + "key": "/apisix/ssls/1" + }]] + ) + + ngx.status = code + ngx.say(body) + } +} +--- response_body +passed + + + +=== TEST 2: set route(id: 1) +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: "edd1c9f0985e76a1" +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: client request with the old style key_encrypt_salt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: "edd1c9f0985e76a1" +--- response_body eval +qr{connected: 1 +ssl handshake: true +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Content-Length: 12 +received: Connection: close +received: Server: APISIX/\d\.\d+(\.\d+)? +received: \nreceived: hello world +close: 1 nil} +--- error_log +server name: "www.test.com" +--- no_error_log +[error] +[alert] + + + +=== TEST 4: client request with the new style key_encrypt_salt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: + - edd1c9f0985e76a1 +--- response_body eval +qr{connected: 1 +ssl handshake: true +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Content-Length: 12 +received: Connection: close +received: Server: APISIX/\d\.\d+(\.\d+)? +received: \nreceived: hello world +close: 1 nil} +--- error_log +server name: "www.test.com" +--- no_error_log +[error] +[alert] + + + +=== TEST 5: client request failed with the wrong key_encrypt_salt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: + - edd1c9f0985e76a2 +--- error_log +decrypt ssl key failed +[alert] + + + +=== TEST 6: client request successfully, use the two key_encrypt_salt to decrypt in turn +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: + - edd1c9f0985e76a2 + - edd1c9f0985e76a1 +--- response_body eval +qr{connected: 1 +ssl handshake: true +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Content-Length: 12 +received: Connection: close +received: Server: APISIX/\d\.\d+(\.\d+)? +received: \nreceived: hello world +close: 1 nil} +--- error_log +server name: "www.test.com" +[alert] + + + +=== TEST 7: remove test ssl certs +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: + - edd1c9f0985e76a1 +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE) + } +} + + + +=== TEST 8: set ssl(sni: www.test.com), do not encrypt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: null +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + local data = {cert = ssl_cert, key = ssl_key, sni = "www.test.com"} + + local code, body = t.test('/apisix/admin/ssls/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "value": { + "sni": "www.test.com" + }, + "key": "/apisix/ssls/1" + }]] + ) + + ngx.status = code + ngx.say(body) + } +} +--- response_body +passed + + + +=== TEST 9: client request without key_encrypt_salt +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: null +--- response_body eval +qr{connected: 1 +ssl handshake: true +sent http request: 62 bytes. +received: HTTP/1.1 200 OK +received: Content-Type: text/plain +received: Content-Length: 12 +received: Connection: close +received: Server: APISIX/\d\.\d+(\.\d+)? +received: \nreceived: hello world +close: 1 nil} +--- error_log +server name: "www.test.com" +--- no_error_log +[error] +[alert] + + + +=== TEST 10: remove test ssl certs +--- yaml_config +apisix: + node_listen: 1984 + ssl: + key_encrypt_salt: null +--- config +location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + t.test('/apisix/admin/ssls/1', ngx.HTTP_DELETE) + } +}