Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle host & SNI mismatch in mTLS #8967

Merged
merged 1 commit into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/centos7-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/fuzzing-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 48 additions & 3 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 15 additions & 9 deletions apisix/ssl/router/radixtree_sni.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -153,21 +153,27 @@ 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)

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

Expand Down
2 changes: 2 additions & 0 deletions docs/en/latest/mtls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/latest/mtls.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ apisix:

双向认证是一种密码学安全的验证客户端身份的手段。当你需要加密并保护流量的双向安全时很有用。

* 注意:双向认证只发生在 HTTPS 中。如果你的路由也可以通过 HTTP 访问,你应该在 HTTP 中添加额外的保护,或者禁止通过 HTTP 访问。*

### 如何配置

我们提供了一个[演示教程](./tutorials/client-to-apisix-mtls.md),详细地讲解了如何配置客户端和 APISIX 之间的 mTLS。
Expand Down
159 changes: 159 additions & 0 deletions t/node/client-mtls-openresty.t
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading