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