diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index b395bd724ee3..60ea0a15db7c 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 028befcccabc..ec3701532d77 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 b518f0e30bb2..388af426effe 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 32a326e422a9..fd1f55c395dc 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 c1d6664f150c..eae5a17fe615 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 1160dcc5d2ab..b9168065fd1a 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 af734e9aa17e..050394d1baf5 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 f58df055874e..e3c6386934f3 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