mirror of
https://gitee.com/iresty/apisix.git
synced 2024-12-04 21:17:36 +08:00
feat: support client certificate verification (#4034)
Signed-off-by: spacewander <spacewanderlzx@gmail.com>
This commit is contained in:
parent
93a9feb9b5
commit
544ab52a40
@ -70,6 +70,17 @@ local function check_conf(id, conf, need_id)
|
||||
end
|
||||
end
|
||||
|
||||
if conf.client then
|
||||
if not apisix_ssl.support_client_verification() then
|
||||
return nil, {error_msg = "client tls verify unsupported"}
|
||||
end
|
||||
|
||||
local ok, err = apisix_ssl.validate(conf.client.ca, nil)
|
||||
if not ok then
|
||||
return nil, {error_msg = "failed to validate client_cert: " .. err}
|
||||
end
|
||||
end
|
||||
|
||||
return need_id and id or true
|
||||
end
|
||||
|
||||
|
@ -281,6 +281,19 @@ end
|
||||
|
||||
function _M.http_access_phase()
|
||||
local ngx_ctx = ngx.ctx
|
||||
|
||||
if ngx_ctx.api_ctx and ngx_ctx.api_ctx.ssl_client_verified then
|
||||
local res = ngx_var.ssl_client_verify
|
||||
if res ~= "SUCCESS" then
|
||||
if res == "NONE" then
|
||||
core.log.error("client certificate was not present")
|
||||
else
|
||||
core.log.error("clent certificate verification is not passed: ", res)
|
||||
end
|
||||
return core.response.exit(400)
|
||||
end
|
||||
end
|
||||
|
||||
-- always fetch table from the table pool, we don't need a reused api_ctx
|
||||
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
|
||||
ngx_ctx.api_ctx = api_ctx
|
||||
|
@ -642,6 +642,18 @@ _M.ssl = {
|
||||
type = "array",
|
||||
items = private_key_schema,
|
||||
},
|
||||
client = {
|
||||
type = "object",
|
||||
properties = {
|
||||
ca = certificate_scheme,
|
||||
depth = {
|
||||
type = "integer",
|
||||
minimum = 0,
|
||||
default = 1,
|
||||
},
|
||||
},
|
||||
required = {"ca"},
|
||||
},
|
||||
exptime = {
|
||||
type = "integer",
|
||||
minimum = 1588262400, -- 2020/5/1 0:0:0
|
||||
|
@ -101,6 +101,11 @@ function _M.validate(cert, key)
|
||||
return nil, "failed to parse cert: " .. err
|
||||
end
|
||||
|
||||
if key == nil then
|
||||
-- sometimes we only need to validate the cert
|
||||
return true
|
||||
end
|
||||
|
||||
key = aes_decrypt_pkey(key)
|
||||
if not key then
|
||||
return nil, "failed to decrypt previous encrypted key"
|
||||
@ -152,4 +157,9 @@ function _M.fetch_pkey(sni, pkey)
|
||||
end
|
||||
|
||||
|
||||
function _M.support_client_verification()
|
||||
return ngx_ssl.verify_client ~= nil
|
||||
end
|
||||
|
||||
|
||||
return _M
|
||||
|
@ -20,7 +20,7 @@ local core = require("apisix.core")
|
||||
local apisix_ssl = require("apisix.ssl")
|
||||
local ngx_ssl = require("ngx.ssl")
|
||||
local config_util = require("apisix.core.config_util")
|
||||
local ipairs = ipairs
|
||||
local ipairs = ipairs
|
||||
local type = type
|
||||
local error = error
|
||||
local str_find = core.string.find
|
||||
@ -29,6 +29,7 @@ local ssl_certificates
|
||||
local radixtree_router
|
||||
local radixtree_router_ver
|
||||
|
||||
|
||||
local _M = {
|
||||
version = 0.1,
|
||||
server_name = ngx_ssl.server_name,
|
||||
@ -194,6 +195,24 @@ function _M.match_and_set(api_ctx)
|
||||
end
|
||||
end
|
||||
|
||||
if matched_ssl.value.client then
|
||||
local ca_cert = matched_ssl.value.client.ca
|
||||
local depth = matched_ssl.value.client.depth
|
||||
if apisix_ssl.support_client_verification() then
|
||||
local parsed_cert, err = apisix_ssl.fetch_cert(sni, ca_cert)
|
||||
if not parsed_cert then
|
||||
return false, "failed to parse client cert: " .. err
|
||||
end
|
||||
|
||||
local ok, err = ngx_ssl.verify_client(parsed_cert, depth)
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
|
||||
api_ctx.ssl_client_verified = true
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
@ -784,6 +784,8 @@ Return response from etcd currently.
|
||||
| key | True | Private key | https private key | |
|
||||
| certs | False | An array of certificate | when you need to configure multiple certificate for the same domain, you can pass extra https certificates (excluding the one given as cert) in this field | |
|
||||
| keys | False | An array of private key | https private keys. The keys should be paired with certs above | |
|
||||
| client.ca | False | Certificate| set the CA certificate which will use to verify client. This feature requires OpenResty 1.19+. | |
|
||||
| client.depth | False | Certificate| set the verification depth in the client certificates chain, default to 1. This feature requires OpenResty 1.19+. | |
|
||||
| snis | True | Match Rules | a non-empty arrays of https SNI | |
|
||||
| labels | False | Match Rules | Key/value pairs to specify attributes | {"version":"v2","build":"16","env":"production"} |
|
||||
| create_time | False | Auxiliary | epoch timestamp in second, will be created automatically if missing | 1602883670 |
|
||||
|
@ -781,6 +781,8 @@ $ curl http://127.0.0.1:9080/get
|
||||
| key | 必需 | 私钥 | https 证书私钥 | |
|
||||
| certs | 可选 | 证书字符串数组 | 当你想给同一个域名配置多个证书时,除了第一个证书需要通过 cert 传递外,剩下的证书可以通过该参数传递上来 | |
|
||||
| keys | 可选 | 私钥字符串数组 | certs 对应的证书私钥,注意要跟 certs 一一对应 | |
|
||||
| client.ca | 可选 | 证书| 设置将用于客户端证书校验的 CA 证书。该特性需要 OpenResty 1.19+ | |
|
||||
| client.depth | 可选 | 辅助| 设置客户端证书校验的深度,默认为 1。该特性需要 OpenResty 1.19+ | |
|
||||
| snis | 必需 | 匹配规则 | 非空数组形式,可以匹配多个 SNI | |
|
||||
| labels | 可选 | 匹配规则 | 标识附加属性的键值对 | {"version":"v2","build":"16","env":"production"} |
|
||||
| create_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 |
|
||||
|
306
t/node/client-mtls.t
vendored
Normal file
306
t/node/client-mtls.t
vendored
Normal file
@ -0,0 +1,306 @@
|
||||
#
|
||||
# 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;
|
||||
|
||||
my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
|
||||
my $version = eval { `$nginx_binary -V 2>&1` };
|
||||
|
||||
if ($version !~ m/\/apisix-nginx-module/) {
|
||||
plan(skip_all => "apisix-nginx-module not installed");
|
||||
} else {
|
||||
plan('no_plan');
|
||||
}
|
||||
|
||||
repeat_each(1);
|
||||
log_level('info');
|
||||
no_root_location();
|
||||
no_shuffle();
|
||||
|
||||
add_block_preprocessor(sub {
|
||||
my ($block) = @_;
|
||||
|
||||
if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
|
||||
$block->set_value("no_error_log", "[error]");
|
||||
}
|
||||
});
|
||||
|
||||
run_tests();
|
||||
|
||||
__DATA__
|
||||
|
||||
=== TEST 1: bad client certificate
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local t = require("lib.test_admin")
|
||||
local json = require("toolkit.json")
|
||||
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
|
||||
local ssl_key = t.read_file("t/certs/mtls_client.key")
|
||||
local data = {
|
||||
cert = ssl_cert,
|
||||
key = ssl_key,
|
||||
sni = "test.com",
|
||||
client = {
|
||||
ca = ("test.com"):rep(128),
|
||||
}
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/ssl/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
end
|
||||
ngx.print(body)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
--- error_code: 400
|
||||
--- response_body
|
||||
{"error_msg":"failed to validate client_cert: failed to parse cert: PEM_read_bio_X509_AUX() failed"}
|
||||
|
||||
|
||||
|
||||
=== TEST 2: missing client certificate
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local t = require("lib.test_admin")
|
||||
local json = require("toolkit.json")
|
||||
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
|
||||
local ssl_key = t.read_file("t/certs/mtls_client.key")
|
||||
local data = {
|
||||
cert = ssl_cert,
|
||||
key = ssl_key,
|
||||
sni = "test.com",
|
||||
client = {
|
||||
}
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/ssl/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
end
|
||||
ngx.print(body)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
--- error_code: 400
|
||||
--- response_body
|
||||
{"error_msg":"invalid configuration: property \"client\" validation failed: property \"ca\" is required"}
|
||||
|
||||
|
||||
|
||||
=== TEST 3: set verification
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local t = require("lib.test_admin")
|
||||
local json = require("toolkit.json")
|
||||
local ssl_ca_cert = t.read_file("t/certs/mtls_ca.crt")
|
||||
local ssl_cert = t.read_file("t/certs/mtls_client.crt")
|
||||
local ssl_key = t.read_file("t/certs/mtls_client.key")
|
||||
local data = {
|
||||
upstream = {
|
||||
scheme = "https",
|
||||
type = "roundrobin",
|
||||
nodes = {
|
||||
["127.0.0.1:1994"] = 1,
|
||||
},
|
||||
tls = {
|
||||
client_cert = ssl_cert,
|
||||
client_key = ssl_key,
|
||||
}
|
||||
},
|
||||
plugins = {
|
||||
["proxy-rewrite"] = {
|
||||
uri = "/hello"
|
||||
}
|
||||
},
|
||||
uri = "/mtls"
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/routes/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
ngx.say(body)
|
||||
return
|
||||
end
|
||||
|
||||
local data = {
|
||||
upstream = {
|
||||
type = "roundrobin",
|
||||
nodes = {
|
||||
["127.0.0.1:1980"] = 1,
|
||||
},
|
||||
},
|
||||
uri = "/hello"
|
||||
}
|
||||
assert(t.test('/apisix/admin/routes/2',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
))
|
||||
|
||||
local data = {
|
||||
cert = ssl_cert,
|
||||
key = ssl_key,
|
||||
sni = "localhost",
|
||||
client = {
|
||||
ca = ssl_ca_cert,
|
||||
depth = 2,
|
||||
}
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/ssl/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
end
|
||||
ngx.print(body)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
|
||||
|
||||
|
||||
=== TEST 4: hit
|
||||
--- request
|
||||
GET /mtls
|
||||
--- more_headers
|
||||
Host: localhost
|
||||
--- response_body
|
||||
hello world
|
||||
|
||||
|
||||
|
||||
=== TEST 5: no client certificate
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local t = require("lib.test_admin")
|
||||
local json = require("toolkit.json")
|
||||
local data = {
|
||||
upstream = {
|
||||
scheme = "https",
|
||||
type = "roundrobin",
|
||||
nodes = {
|
||||
["127.0.0.1:1994"] = 1,
|
||||
},
|
||||
},
|
||||
plugins = {
|
||||
["proxy-rewrite"] = {
|
||||
uri = "/hello"
|
||||
}
|
||||
},
|
||||
uri = "/mtls2"
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/routes/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
ngx.say(body)
|
||||
return
|
||||
end
|
||||
|
||||
ngx.print(body)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
|
||||
|
||||
|
||||
=== TEST 6: hit
|
||||
--- request
|
||||
GET /mtls2
|
||||
--- more_headers
|
||||
Host: localhost
|
||||
--- error_code: 400
|
||||
--- error_log
|
||||
client certificate was not present
|
||||
|
||||
|
||||
|
||||
=== TEST 7: wrong client certificate
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local t = require("lib.test_admin")
|
||||
local json = require("toolkit.json")
|
||||
local ssl_cert = t.read_file("t/certs/apisix.crt")
|
||||
local ssl_key = t.read_file("t/certs/apisix.key")
|
||||
local data = {
|
||||
upstream = {
|
||||
scheme = "https",
|
||||
type = "roundrobin",
|
||||
nodes = {
|
||||
["127.0.0.1:1994"] = 1,
|
||||
},
|
||||
tls = {
|
||||
client_cert = ssl_cert,
|
||||
client_key = ssl_key,
|
||||
}
|
||||
},
|
||||
plugins = {
|
||||
["proxy-rewrite"] = {
|
||||
uri = "/hello"
|
||||
}
|
||||
},
|
||||
uri = "/mtls3"
|
||||
}
|
||||
local code, body = t.test('/apisix/admin/routes/1',
|
||||
ngx.HTTP_PUT,
|
||||
json.encode(data)
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
ngx.say(body)
|
||||
return
|
||||
end
|
||||
|
||||
ngx.print(body)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
|
||||
|
||||
|
||||
=== TEST 8: hit
|
||||
--- request
|
||||
GET /mtls3
|
||||
--- more_headers
|
||||
Host: localhost
|
||||
--- error_code: 400
|
||||
--- error_log
|
||||
clent certificate verification is not passed: FAILED:self signed certificate
|
Loading…
Reference in New Issue
Block a user