From d58bc661e7f740eddaa285663601b23611695c47 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 01:11:23 +0530 Subject: [PATCH 1/7] feat: add support for wildcard on SNIs for SSL --- apisix/schema_def.lua | 2 +- apisix/ssl/router/radixtree_sni.lua | 69 ++++++++++++++++++++++------- t/stream-node/sni.t | 68 ++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 18 deletions(-) diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index cff101007fd6..c13b6bcb1b01 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -39,7 +39,7 @@ local id_schema = { } } -local host_def_pat = "^\\*?[0-9a-zA-Z-._\\[\\]:]+$" +local host_def_pat = "^\\*$|^\\*?[0-9a-zA-Z-._\\[\\]:]+$" local host_def = { type = "string", pattern = host_def_pat, diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index ae7e5b265bf9..3df649bde34d 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -55,24 +55,30 @@ local function create_router(ssl_items) if type(ssl.value.snis) == "table" and #ssl.value.snis > 0 then sni = core.table.new(0, #ssl.value.snis) for _, s in ipairs(ssl.value.snis) do - j = j + 1 - sni[j] = s:reverse() + if s ~= "*" then + j = j + 1 + sni[j] = s:reverse() + end end else - sni = ssl.value.sni:reverse() + if ssl.value.sni ~= "*" then + sni = ssl.value.sni:reverse() + end end - idx = idx + 1 - route_items[idx] = { - paths = sni, - handler = function (api_ctx) - if not api_ctx then - return + if sni and (type(sni) == "table" and #sni > 0 or type(sni) == "string") then + idx = idx + 1 + route_items[idx] = { + paths = sni, + handler = function (api_ctx) + if not api_ctx then + return + end + api_ctx.matched_ssl = ssl + api_ctx.matched_sni = sni end - api_ctx.matched_ssl = ssl - api_ctx.matched_sni = sni - end - } + } + end end end @@ -89,7 +95,6 @@ local function create_router(ssl_items) return router end - local function set_pem_ssl_key(sni, cert, pkey) local r = get_request() if r == nil then @@ -171,6 +176,33 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) local sni_rev = sni:reverse() local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx) + + -- if no SSL matched, try to find a wildcard SSL + if not ok then + for _, ssl in config_util.iterate_values(ssl_certificates.values) do + if ssl.value and ssl.value.type == "server" and + (ssl.value.status == nil or ssl.value.status == 1) then + local has_wildcard = false + if ssl.value.sni == "*" then + has_wildcard = true + elseif type(ssl.value.snis) == "table" then + for _, s in ipairs(ssl.value.snis) do + if s == "*" then + has_wildcard = true + break + end + end + end + if has_wildcard then + api_ctx.matched_ssl = ssl + api_ctx.matched_sni = "*" + ok = true + break + end + end + end + end + if not ok then if not alt_sni then -- it is expected that alternative SNI doesn't have a SSL certificate associated @@ -180,8 +212,11 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return false end - - if type(api_ctx.matched_sni) == "table" then + if api_ctx.matched_sni == "*" then + -- wildcard matches everything, no need for further validation + core.log.info("matched wildcard SSL for SNI: ", sni) + elseif type(api_ctx.matched_sni) == "table" then + -- Existing logic for multiple SNIs local matched = false for _, msni in ipairs(api_ctx.matched_sni) do if sni_rev == msni or not str_find(sni_rev, ".", #msni) then @@ -200,6 +235,7 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return false end else + -- Existing logic for single SNI if str_find(sni_rev, ".", #api_ctx.matched_sni) then core.log.warn("failed to find any SSL certificate by SNI: ", sni, " matched SNI: ", api_ctx.matched_sni:reverse()) @@ -221,7 +257,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return true end - function _M.set(matched_ssl, sni) if not matched_ssl then return false, "failed to match ssl certificate" diff --git a/t/stream-node/sni.t b/t/stream-node/sni.t index 41554ba6c0c2..9457e6c436f2 100644 --- a/t/stream-node/sni.t +++ b/t/stream-node/sni.t @@ -339,3 +339,71 @@ proxy request to 127.0.0.3:1995 } --- request GET /t + + +=== TEST 14: set SSL with wildcard * SNI and test route matching +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + + -- Create SSL with wildcard * SNI (catch-all) + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "*", -- Wildcard catch-all + } + + local code, body = t.test('/apisix/admin/ssls/100', + ngx.HTTP_PUT, + core.json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create wildcard SSL: ", code, " ", body) + return + end + + -- Create a stream route that will use the wildcard SSL + local code, body = t.test('/apisix/admin/stream_routes/100', + ngx.HTTP_PUT, + [[{ + "sni": "unknown-domain.com", + "upstream": { + "nodes": { + "127.0.0.1:1995": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create stream route: ", code, " ", body) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 15: hit route with unknown domain using wildcard SSL +--- stream_tls_request +mmm +--- stream_sni: unknown-domain.com +--- response_body +hello world +--- error_log +proxy request to 127.0.0.1:1995 From 25913089a960db14aef0fe7895fe1b7edef61b08 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 01:17:02 +0530 Subject: [PATCH 2/7] f --- apisix/ssl/router/radixtree_sni.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 3df649bde34d..27862c6ae546 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -216,7 +216,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) -- wildcard matches everything, no need for further validation core.log.info("matched wildcard SSL for SNI: ", sni) elseif type(api_ctx.matched_sni) == "table" then - -- Existing logic for multiple SNIs local matched = false for _, msni in ipairs(api_ctx.matched_sni) do if sni_rev == msni or not str_find(sni_rev, ".", #msni) then @@ -235,7 +234,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return false end else - -- Existing logic for single SNI if str_find(sni_rev, ".", #api_ctx.matched_sni) then core.log.warn("failed to find any SSL certificate by SNI: ", sni, " matched SNI: ", api_ctx.matched_sni:reverse()) From 51b294ce48c5e5babba5bb627653d7de5276b7bc Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 11:53:24 +0530 Subject: [PATCH 3/7] add test --- t/stream-node/sni.t | 108 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/t/stream-node/sni.t b/t/stream-node/sni.t index 9457e6c436f2..a92b92e62c70 100644 --- a/t/stream-node/sni.t +++ b/t/stream-node/sni.t @@ -407,3 +407,111 @@ mmm hello world --- error_log proxy request to 127.0.0.1:1995 + + + +=== TEST 16: test SSL priority - exact match over partial wildcard +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + + -- Create SSL with exact domain match + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "specific.api7.dev", + } + + local code, body = t.test('/apisix/admin/ssls/101', + ngx.HTTP_PUT, + core.json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create exact SSL: ", code, " ", body) + return + end + + -- Create SSL with partial wildcard + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "*.api7.dev", + } + + local code, body = t.test('/apisix/admin/ssls/102', + ngx.HTTP_PUT, + core.json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create partial wildcard SSL: ", code, " ", body) + return + end + + -- Create routes for testing + local code, body = t.test('/apisix/admin/stream_routes/101', + ngx.HTTP_PUT, + [[{ + "sni": "specific.api7.dev", + "upstream": { + "nodes": { + "127.0.0.1:1995": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create exact route: ", code, " ", body) + return + end + + local code, body = t.test('/apisix/admin/stream_routes/102', + ngx.HTTP_PUT, + [[{ + "sni": "*.api7.dev", + "upstream": { + "nodes": { + "127.0.0.2:1995": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create partial wildcard route: ", code, " ", body) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 17: verify exact domain takes priority over partial wildcard +--- stream_tls_request +mmm +--- stream_sni: specific.api7.dev +--- response_body +hello world +--- error_log +proxy request to 127.0.0.1:1995 +--- no_error_log +proxy request to 127.0.0.2:1995 From 5485fae962f9299b11fb0e213f16e8beab624cae Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 12:03:31 +0530 Subject: [PATCH 4/7] fix lint --- t/stream-node/sni.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/stream-node/sni.t b/t/stream-node/sni.t index a92b92e62c70..3ddf8a1788e0 100644 --- a/t/stream-node/sni.t +++ b/t/stream-node/sni.t @@ -341,6 +341,7 @@ proxy request to 127.0.0.3:1995 GET /t + === TEST 14: set SSL with wildcard * SNI and test route matching --- config location /t { From bc67323aa7a594e35cf817769e549184a515c4c2 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 12:14:32 +0530 Subject: [PATCH 5/7] add more tests --- t/stream-node/sni.t | 108 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/t/stream-node/sni.t b/t/stream-node/sni.t index 3ddf8a1788e0..b4f3e28bcc05 100644 --- a/t/stream-node/sni.t +++ b/t/stream-node/sni.t @@ -516,3 +516,111 @@ hello world proxy request to 127.0.0.1:1995 --- no_error_log proxy request to 127.0.0.2:1995 + + + +=== TEST 18: test SSL priority - partial match over wildcard +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + + -- Create SSL with partial domain match + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "specific.api7.dev", + } + + local code, body = t.test('/apisix/admin/ssls/101', + ngx.HTTP_PUT, + core.json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create exact SSL: ", code, " ", body) + return + end + + -- Create SSL with wildcard + local data = { + cert = ssl_cert, + key = ssl_key, + sni = "*", + } + + local code, body = t.test('/apisix/admin/ssls/102', + ngx.HTTP_PUT, + core.json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create partial wildcard SSL: ", code, " ", body) + return + end + + -- Create routes for testing + local code, body = t.test('/apisix/admin/stream_routes/101', + ngx.HTTP_PUT, + [[{ + "sni": "*.api7.dev", + "upstream": { + "nodes": { + "127.0.0.1:1995": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create exact route: ", code, " ", body) + return + end + + local code, body = t.test('/apisix/admin/stream_routes/102', + ngx.HTTP_PUT, + [[{ + "sni": "*", + "upstream": { + "nodes": { + "127.0.0.2:1995": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("failed to create partial wildcard route: ", code, " ", body) + return + end + + ngx.say("passed") + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 19: verify partial match takes priority over wildcard +--- stream_tls_request +mmm +--- stream_sni: specific.api7.dev +--- response_body +hello world +--- error_log +proxy request to 127.0.0.1:1995 +--- no_error_log +proxy request to 127.0.0.2:1995 From 2ac6d23d578d72389518973c4b3f7c3abd786ce0 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 13:38:49 +0530 Subject: [PATCH 6/7] f --- apisix/ssl/router/radixtree_sni.lua | 63 ++++++++--------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 27862c6ae546..84ca47ab3789 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -1,3 +1,4 @@ + -- -- Licensed to the Apache Software Foundation (ASF) under one or more -- contributor license agreements. See the NOTICE file distributed with @@ -55,30 +56,24 @@ local function create_router(ssl_items) if type(ssl.value.snis) == "table" and #ssl.value.snis > 0 then sni = core.table.new(0, #ssl.value.snis) for _, s in ipairs(ssl.value.snis) do - if s ~= "*" then - j = j + 1 - sni[j] = s:reverse() - end + j = j + 1 + sni[j] = s:reverse() end else - if ssl.value.sni ~= "*" then - sni = ssl.value.sni:reverse() - end + sni = ssl.value.sni:reverse() end - if sni and (type(sni) == "table" and #sni > 0 or type(sni) == "string") then - idx = idx + 1 - route_items[idx] = { - paths = sni, - handler = function (api_ctx) - if not api_ctx then - return - end - api_ctx.matched_ssl = ssl - api_ctx.matched_sni = sni + idx = idx + 1 + route_items[idx] = { + paths = sni, + handler = function (api_ctx) + if not api_ctx then + return end - } - end + api_ctx.matched_ssl = ssl + api_ctx.matched_sni = sni + end + } end end @@ -95,6 +90,7 @@ local function create_router(ssl_items) return router end + local function set_pem_ssl_key(sni, cert, pkey) local r = get_request() if r == nil then @@ -176,33 +172,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) local sni_rev = sni:reverse() local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx) - - -- if no SSL matched, try to find a wildcard SSL - if not ok then - for _, ssl in config_util.iterate_values(ssl_certificates.values) do - if ssl.value and ssl.value.type == "server" and - (ssl.value.status == nil or ssl.value.status == 1) then - local has_wildcard = false - if ssl.value.sni == "*" then - has_wildcard = true - elseif type(ssl.value.snis) == "table" then - for _, s in ipairs(ssl.value.snis) do - if s == "*" then - has_wildcard = true - break - end - end - end - if has_wildcard then - api_ctx.matched_ssl = ssl - api_ctx.matched_sni = "*" - ok = true - break - end - end - end - end - if not ok then if not alt_sni then -- it is expected that alternative SNI doesn't have a SSL certificate associated @@ -212,6 +181,7 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return false end + if api_ctx.matched_sni == "*" then -- wildcard matches everything, no need for further validation core.log.info("matched wildcard SSL for SNI: ", sni) @@ -255,6 +225,7 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) return true end + function _M.set(matched_ssl, sni) if not matched_ssl then return false, "failed to match ssl certificate" From 7f42da071d1b27dea73212c73b292db375760cb3 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 14 Oct 2025 14:49:06 +0530 Subject: [PATCH 7/7] Update radixtree_sni.lua --- apisix/ssl/router/radixtree_sni.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 84ca47ab3789..f12be1298680 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -1,4 +1,3 @@ - -- -- Licensed to the Apache Software Foundation (ASF) under one or more -- contributor license agreements. See the NOTICE file distributed with