You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2023/03/01 09:49:27 UTC
[apisix] branch master updated: fix: handle host & SNI mismatch in mTLS (#8967)
This is an automated email from the ASF dual-hosted git repository.
spacewander pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 01e9c1ae8 fix: handle host & SNI mismatch in mTLS (#8967)
01e9c1ae8 is described below
commit 01e9c1ae8671367d5ff22523b2706546bee6292f
Author: 罗泽轩 <sp...@gmail.com>
AuthorDate: Wed Mar 1 17:49:21 2023 +0800
fix: handle host & SNI mismatch in mTLS (#8967)
---
.github/workflows/centos7-ci.yml | 2 +-
.github/workflows/fuzzing-ci.yaml | 1 +
apisix/init.lua | 51 ++++++++-
apisix/ssl/router/radixtree_sni.lua | 24 +++--
docs/en/latest/mtls.md | 2 +
docs/zh/latest/mtls.md | 2 +
t/node/client-mtls-openresty.t | 159 ++++++++++++++++++++++++++++
t/node/client-mtls.t | 203 ++++++++++++++++++++++++++++++++++++
8 files changed, 431 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml
index b395bd724..60ea0a15d 100644
--- a/.github/workflows/centos7-ci.yml
+++ b/.github/workflows/centos7-ci.yml
@@ -100,7 +100,7 @@ jobs:
env:
TEST_FILE_SUB_DIR: ${{ matrix.test_dir }}
run: |
- docker run -itd -v /home/runner/work/apisix/apisix:/apisix --env TEST_FILE_SUB_DIR="$TEST_FILE_SUB_DIR" --name centos7Instance --net="host" --dns 8.8.8.8 --dns-search apache.org docker.io/centos:7 /bin/bash
+ docker run -itd -v ${{ github.workspace }}:/apisix --env TEST_FILE_SUB_DIR="$TEST_FILE_SUB_DIR" --name centos7Instance --net="host" --dns 8.8.8.8 --dns-search apache.org docker.io/centos:7 /bin/bash
# docker exec centos7Instance bash -c "cp -r /tmp/apisix ./"
- name: Cache images
diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml
index 028befccc..ec3701532 100644
--- a/.github/workflows/fuzzing-ci.yaml
+++ b/.github/workflows/fuzzing-ci.yaml
@@ -69,6 +69,7 @@ jobs:
- name: run tests
run: |
+ export APISIX_FUZZING_PWD=$PWD
python $PWD/t/fuzzing/simpleroute_test.py
python $PWD/t/fuzzing/serverless_route_test.py
python $PWD/t/fuzzing/vars_route_test.py
diff --git a/apisix/init.lua b/apisix/init.lua
index b518f0e30..388af426e 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -304,6 +304,51 @@ local function verify_tls_client(ctx)
end
+local function verify_https_client(ctx)
+ local scheme = ctx.var.scheme
+ if scheme ~= "https" then
+ return true
+ end
+
+ local host = ctx.var.host
+ local matched = router.router_ssl.match_and_set(ctx, true, host)
+ if not matched then
+ return true
+ end
+
+ local matched_ssl = ctx.matched_ssl
+ if matched_ssl.value.client and apisix_ssl.support_client_verification() then
+ local verified = apisix_base_flags.client_cert_verified_in_handshake
+ if not verified then
+ -- vanilla OpenResty requires to check the verification result
+ local res = ctx.var.ssl_client_verify
+ if res ~= "SUCCESS" then
+ if res == "NONE" then
+ core.log.error("client certificate was not present")
+ else
+ core.log.error("client certificate verification is not passed: ", res)
+ end
+
+ return false
+ end
+ end
+
+ local sni = apisix_ssl.server_name()
+ if sni ~= host then
+ -- There is a case that the user configures a SSL object with `*.domain`,
+ -- and the client accesses with SNI `a.domain` but uses Host `b.domain`.
+ -- This case is complex and we choose to restrict the access until there
+ -- is a stronge demand in real world.
+ core.log.error("client certificate verified with SNI ", sni,
+ ", but the host is ", host)
+ return false
+ end
+ end
+
+ return true
+end
+
+
local function normalize_uri_like_servlet(uri)
local found = core.string.find(uri, ';')
if not found then
@@ -475,12 +520,12 @@ function _M.http_access_phase()
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
ngx_ctx.api_ctx = api_ctx
- if not verify_tls_client(api_ctx) then
+ core.ctx.set_vars_meta(api_ctx)
+
+ if not verify_https_client(api_ctx) then
return core.response.exit(400)
end
- core.ctx.set_vars_meta(api_ctx)
-
debug.dynamic_debug(api_ctx)
local uri = api_ctx.var.uri
diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua
index 32a326e42..fd1f55c39 100644
--- a/apisix/ssl/router/radixtree_sni.lua
+++ b/apisix/ssl/router/radixtree_sni.lua
@@ -142,7 +142,7 @@ function _M.set_cert_and_key(sni, value)
end
-function _M.match_and_set(api_ctx, match_only)
+function _M.match_and_set(api_ctx, match_only, alt_sni)
local err
if not radixtree_router or
radixtree_router_ver ~= ssl_certificates.conf_version then
@@ -153,13 +153,15 @@ function _M.match_and_set(api_ctx, match_only)
radixtree_router_ver = ssl_certificates.conf_version
end
- local sni
- sni, err = apisix_ssl.server_name()
- if type(sni) ~= "string" then
- local advise = "please check if the client requests via IP or uses an outdated protocol" ..
- ". If you need to report an issue, " ..
- "provide a packet capture file of the TLS handshake."
- return false, "failed to find SNI: " .. (err or advise)
+ local sni = alt_sni
+ if not sni then
+ sni, err = apisix_ssl.server_name()
+ if type(sni) ~= "string" then
+ local advise = "please check if the client requests via IP or uses an outdated " ..
+ "protocol. If you need to report an issue, " ..
+ "provide a packet capture file of the TLS handshake."
+ return false, "failed to find SNI: " .. (err or advise)
+ end
end
core.log.debug("sni: ", sni)
@@ -167,7 +169,11 @@ function _M.match_and_set(api_ctx, match_only)
local sni_rev = sni:reverse()
local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx)
if not ok then
- core.log.error("failed to find any SSL certificate by SNI: ", sni)
+ if not alt_sni then
+ -- it is expected that alternative SNI doesn't have a SSL certificate associated
+ -- with it sometimes
+ core.log.error("failed to find any SSL certificate by SNI: ", sni)
+ end
return false
end
diff --git a/docs/en/latest/mtls.md b/docs/en/latest/mtls.md
index c1d6664f1..eae5a17fe 100644
--- a/docs/en/latest/mtls.md
+++ b/docs/en/latest/mtls.md
@@ -95,6 +95,8 @@ apisix:
Using mTLS is a way to verify clients cryptographically. It is useful and important in cases where you want to have encrypted and secure traffic in both directions.
+* Note: the mTLS protection only happens in HTTPS. If your route can also be accessed via HTTP, you should add additional protection in HTTP or disable the access via HTTP.*
+
### How to configure
We provide a [tutorial](./tutorials/client-to-apisix-mtls.md) that explains in detail how to configure mTLS between the client and APISIX.
diff --git a/docs/zh/latest/mtls.md b/docs/zh/latest/mtls.md
index 1160dcc5d..b9168065f 100644
--- a/docs/zh/latest/mtls.md
+++ b/docs/zh/latest/mtls.md
@@ -95,6 +95,8 @@ apisix:
双向认证是一种密码学安全的验证客户端身份的手段。当你需要加密并保护流量的双向安全时很有用。
+* 注意:双向认证只发生在 HTTPS 中。如果你的路由也可以通过 HTTP 访问,你应该在 HTTP 中添加额外的保护,或者禁止通过 HTTP 访问。*
+
### 如何配置
我们提供了一个[演示教程](./tutorials/client-to-apisix-mtls.md),详细地讲解了如何配置客户端和 APISIX 之间的 mTLS。
diff --git a/t/node/client-mtls-openresty.t b/t/node/client-mtls-openresty.t
index af734e9aa..050394d1b 100644
--- a/t/node/client-mtls-openresty.t
+++ b/t/node/client-mtls-openresty.t
@@ -111,3 +111,162 @@ curl --cert t/certs/apisix.crt --key t/certs/apisix.key -k https://localhost:199
qr/400 Bad Request/
--- error_log eval
qr/client certificate verification is not passed: FAILED:self[- ]signed certificate/
+
+
+
+=== TEST 5: hit with different host which doesn't require mTLS
+--- exec
+curl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H "Host: test.com"
+--- response_body
+hello world
+
+
+
+=== TEST 6: set verification (2 ssl objects)
+--- 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 = {
+ type = "roundrobin",
+ nodes = {
+ ["127.0.0.1:1980"] = 1,
+ },
+ },
+ uri = "/hello"
+ }
+ assert(t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ ))
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "test.com",
+ client = {
+ ca = ssl_ca_cert,
+ depth = 2,
+ }
+ }
+ local code, body = t.test('/apisix/admin/ssls/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "localhost",
+ }
+ local code, body = t.test('/apisix/admin/ssls/2',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+
+
+
+=== TEST 7: hit without mTLS verify, with Host requires mTLS verification
+--- exec
+curl -k https://localhost:1994/hello -H "Host: test.com"
+--- response_body eval
+qr/400 Bad Request/
+--- error_log
+client certificate was not present
+
+
+
+=== TEST 8: set verification (2 ssl objects, both have mTLS)
+--- 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_ca_cert2 = t.read_file("t/certs/apisix.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 = {
+ type = "roundrobin",
+ nodes = {
+ ["127.0.0.1:1980"] = 1,
+ },
+ },
+ uri = "/hello"
+ }
+ assert(t.test('/apisix/admin/routes/1',
+ 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/ssls/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "test.com",
+ client = {
+ ca = ssl_ca_cert2,
+ depth = 2,
+ }
+ }
+ local code, body = t.test('/apisix/admin/ssls/2',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+
+
+
+=== TEST 9: hit with mTLS verify, with Host requires different mTLS verification
+--- exec
+curl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H "Host: test.com"
+--- response_body eval
+qr/400 Bad Request/
+--- error_log
+client certificate verified with SNI localhost, but the host is test.com
diff --git a/t/node/client-mtls.t b/t/node/client-mtls.t
index f58df0558..e3c638693 100644
--- a/t/node/client-mtls.t
+++ b/t/node/client-mtls.t
@@ -300,3 +300,206 @@ Host: localhost
--- error_code: 502
--- error_log
certificate verify failed
+
+
+
+=== TEST 9: 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 = {
+ type = "roundrobin",
+ nodes = {
+ ["127.0.0.1:1980"] = 1,
+ },
+ },
+ uri = "/hello"
+ }
+ assert(t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ ))
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "localhost",
+ }
+ local code, body = t.test('/apisix/admin/ssls/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+
+
+
+=== TEST 10: hit with different host which doesn't require mTLS
+--- exec
+curl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H "Host: x.com"
+--- response_body
+hello world
+
+
+
+=== TEST 11: set verification (2 ssl objects)
+--- 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 = {
+ type = "roundrobin",
+ nodes = {
+ ["127.0.0.1:1980"] = 1,
+ },
+ },
+ uri = "/hello"
+ }
+ assert(t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ ))
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "test.com",
+ client = {
+ ca = ssl_ca_cert,
+ depth = 2,
+ }
+ }
+ local code, body = t.test('/apisix/admin/ssls/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "localhost",
+ }
+ local code, body = t.test('/apisix/admin/ssls/2',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+
+
+
+=== TEST 12: hit without mTLS verify, with Host requires mTLS verification
+--- exec
+curl -k https://localhost:1994/hello -H "Host: test.com"
+--- response_body eval
+qr/400 Bad Request/
+--- error_log
+client certificate verified with SNI localhost, but the host is test.com
+
+
+
+=== TEST 13: set verification (2 ssl objects, both have mTLS)
+--- 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_ca_cert2 = t.read_file("t/certs/apisix.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 = {
+ type = "roundrobin",
+ nodes = {
+ ["127.0.0.1:1980"] = 1,
+ },
+ },
+ uri = "/hello"
+ }
+ assert(t.test('/apisix/admin/routes/1',
+ 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/ssls/1',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+
+ local data = {
+ cert = ssl_cert,
+ key = ssl_key,
+ sni = "test.com",
+ client = {
+ ca = ssl_ca_cert2,
+ depth = 2,
+ }
+ }
+ local code, body = t.test('/apisix/admin/ssls/2',
+ ngx.HTTP_PUT,
+ json.encode(data)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.print(body)
+ }
+ }
+--- request
+GET /t
+
+
+
+=== TEST 14: hit with mTLS verify, with Host requires different mTLS verification
+--- exec
+curl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:1994/hello -H "Host: test.com"
+--- response_body eval
+qr/400 Bad Request/
+--- error_log
+client certificate verified with SNI localhost, but the host is test.com