This is an automated email from the ASF dual-hosted git repository.
baoyuan 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 2e0b6b2a6 fix: address TLS security vulnerabilities in SSL log, OIDC
encryption, and K8s ssl_verify (#13190)
2e0b6b2a6 is described below
commit 2e0b6b2a69c66fb10b33b947dbdcbfa5699e6f75
Author: AlinsRan <[email protected]>
AuthorDate: Mon Apr 13 11:24:54 2026 +0800
fix: address TLS security vulnerabilities in SSL log, OIDC encryption, and
K8s ssl_verify (#13190)
---
apisix/discovery/kubernetes/informer_factory.lua | 2 +-
apisix/discovery/kubernetes/init.lua | 7 +
apisix/discovery/kubernetes/schema.lua | 12 ++
apisix/ssl/router/radixtree_sni.lua | 2 -
t/discovery/kubernetes_ssl_verify.t | 217 +++++++++++++++++++++++
t/kubernetes/discovery/kubernetes2.t | 1 +
t/plugin/openid-connect2.t | 80 +++++++++
t/router/radixtree-sni3.t | 36 ++++
8 files changed, 354 insertions(+), 3 deletions(-)
diff --git a/apisix/discovery/kubernetes/informer_factory.lua
b/apisix/discovery/kubernetes/informer_factory.lua
index d31841b1e..4d650d7bb 100644
--- a/apisix/discovery/kubernetes/informer_factory.lua
+++ b/apisix/discovery/kubernetes/informer_factory.lua
@@ -273,7 +273,7 @@ local function list_watch(informer, apiserver)
scheme = apiserver.schema,
host = apiserver.host,
port = apiserver.port,
- ssl_verify = false
+ ssl_verify = apiserver.ssl_verify
})
if not ok then
diff --git a/apisix/discovery/kubernetes/init.lua
b/apisix/discovery/kubernetes/init.lua
index 0b31c392a..a6cf828e2 100644
--- a/apisix/discovery/kubernetes/init.lua
+++ b/apisix/discovery/kubernetes/init.lua
@@ -479,6 +479,13 @@ local function get_apiserver(conf)
return nil, "apiserver.token should set to non-empty string when
service.schema is https"
end
+ -- ssl_verify: use explicit config if set, otherwise default to false
+ if conf.service.ssl_verify ~= nil then
+ apiserver.ssl_verify = conf.service.ssl_verify
+ else
+ apiserver.ssl_verify = false
+ end
+
return apiserver
end
diff --git a/apisix/discovery/kubernetes/schema.lua
b/apisix/discovery/kubernetes/schema.lua
index 780aad255..b3f35ae72 100644
--- a/apisix/discovery/kubernetes/schema.lua
+++ b/apisix/discovery/kubernetes/schema.lua
@@ -129,6 +129,12 @@ return {
oneOf = port_patterns,
default = "${KUBERNETES_SERVICE_PORT}",
},
+ ssl_verify = {
+ type = "boolean",
+ description = "Verify the TLS certificate of the
Kubernetes API " ..
+ "server. Defaults to false. Set to
true to enable " ..
+ "certificate verification.",
+ },
},
default = {
schema = "https",
@@ -190,6 +196,12 @@ return {
type = "string",
oneOf = port_patterns,
},
+ ssl_verify = {
+ type = "boolean",
+ description = "Verify the TLS certificate of
the Kubernetes " ..
+ "API server. Defaults to false.
Set to true to " ..
+ "enable certificate
verification.",
+ },
},
required = { "host", "port" }
},
diff --git a/apisix/ssl/router/radixtree_sni.lua
b/apisix/ssl/router/radixtree_sni.lua
index 15ea67225..07ad262ba 100644
--- a/apisix/ssl/router/radixtree_sni.lua
+++ b/apisix/ssl/router/radixtree_sni.lua
@@ -214,8 +214,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni)
end
end
- core.log.info("debug - matched: ",
core.json.delay_encode(api_ctx.matched_ssl, true))
-
if match_only then
return true
end
diff --git a/t/discovery/kubernetes_ssl_verify.t
b/t/discovery/kubernetes_ssl_verify.t
new file mode 100644
index 000000000..68571797e
--- /dev/null
+++ b/t/discovery/kubernetes_ssl_verify.t
@@ -0,0 +1,217 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+log_level("info");
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if (!$block->error_log && !$block->no_error_log) {
+ $block->set_value("no_error_log", "[error]\n[alert]");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: ssl_verify is false when apiserver uses http scheme (no explicit
config)
+--- config
+ location /t {
+ content_by_lua_block {
+ local captured_opts = {}
+
+ -- Monkeypatch resty.http to capture connect options
+ local http_orig = require("resty.http")
+ local http_new_orig = http_orig.new
+ http_orig.new = function()
+ local mock = {}
+ mock.connect = function(self, opts)
+ captured_opts.ssl_verify = opts and opts.ssl_verify
+ -- simulate connection refused to stop further processing
+ return false, "connection refused"
+ end
+ return mock
+ end
+
+ local factory =
require("apisix.discovery.kubernetes.informer_factory")
+ local informer = factory.new(nil, "v1", "Endpoints", "endpoints",
nil)
+
+ -- apiserver.ssl_verify is set by init.lua get_apiserver; simulate
http result
+ local apiserver = {
+ schema = "http",
+ host = "127.0.0.1",
+ port = 6445,
+ ssl_verify = false, -- what get_apiserver would set for http
scheme
+ }
+
+ informer:list_watch(apiserver)
+ ngx.say("ssl_verify for http scheme: ",
tostring(captured_opts.ssl_verify))
+
+ -- restore
+ http_orig.new = http_new_orig
+ }
+ }
+--- response_body
+ssl_verify for http scheme: false
+--- no_error_log
+[alert]
+
+
+
+=== TEST 2: ssl_verify defaults to false when apiserver uses https scheme (no
explicit config)
+--- config
+ location /t {
+ content_by_lua_block {
+ local captured_opts = {}
+
+ -- Monkeypatch resty.http to capture connect options
+ local http_orig = require("resty.http")
+ local http_new_orig = http_orig.new
+ http_orig.new = function()
+ local mock = {}
+ mock.connect = function(self, opts)
+ captured_opts.ssl_verify = opts and opts.ssl_verify
+ -- simulate connection refused to stop further processing
+ return false, "connection refused"
+ end
+ return mock
+ end
+
+ local factory =
require("apisix.discovery.kubernetes.informer_factory")
+ local informer = factory.new(nil, "v1", "Endpoints", "endpoints",
nil)
+
+ -- apiserver.ssl_verify is set by init.lua get_apiserver; default
is false
+ local apiserver = {
+ schema = "https",
+ host = "127.0.0.1",
+ port = 6443,
+ ssl_verify = false, -- default when no explicit config, even
for https
+ }
+
+ informer:list_watch(apiserver)
+ ngx.say("ssl_verify for https scheme (no config): ",
tostring(captured_opts.ssl_verify))
+
+ -- restore
+ http_orig.new = http_new_orig
+ }
+ }
+--- response_body
+ssl_verify for https scheme (no config): false
+--- no_error_log
+[alert]
+
+
+
+=== TEST 3: explicit ssl_verify=true enables certificate verification for https
+--- config
+ location /t {
+ content_by_lua_block {
+ local captured_opts = {}
+
+ -- Monkeypatch resty.http to capture connect options
+ local http_orig = require("resty.http")
+ local http_new_orig = http_orig.new
+ http_orig.new = function()
+ local mock = {}
+ mock.connect = function(self, opts)
+ captured_opts.ssl_verify = opts and opts.ssl_verify
+ return false, "connection refused"
+ end
+ return mock
+ end
+
+ local factory =
require("apisix.discovery.kubernetes.informer_factory")
+ local informer = factory.new(nil, "v1", "Endpoints", "endpoints",
nil)
+
+ -- Simulate get_apiserver with explicit ssl_verify=true (user opts
in)
+ local apiserver = {
+ schema = "https",
+ host = "127.0.0.1",
+ port = 6443,
+ ssl_verify = true, -- explicit opt-in for certificate
verification
+ }
+
+ informer:list_watch(apiserver)
+ ngx.say("explicit ssl_verify=true respected: ",
tostring(captured_opts.ssl_verify == true))
+
+ -- restore
+ http_orig.new = http_new_orig
+ }
+ }
+--- response_body
+explicit ssl_verify=true respected: true
+--- no_error_log
+[alert]
+
+
+
+=== TEST 4: get_apiserver defaults ssl_verify to false when service.ssl_verify
is not configured
+--- config
+ location /t {
+ content_by_lua_block {
+ -- Verify the contract of get_apiserver() in init.lua:
+ -- when conf.service.ssl_verify is nil (not configured),
ssl_verify must be false.
+ -- This is the backward-compatible default — NOT derived from the
scheme.
+ --
+ -- The logic being tested (init.lua):
+ -- if conf.service.ssl_verify ~= nil then
+ -- apiserver.ssl_verify = conf.service.ssl_verify
+ -- else
+ -- apiserver.ssl_verify = false <-- must be false, not
(schema == "https")
+ -- end
+
+ local function compute_ssl_verify(service_conf)
+ if service_conf.ssl_verify ~= nil then
+ return service_conf.ssl_verify
+ else
+ return false
+ end
+ end
+
+ -- Case 1: https with no ssl_verify set -> must be false
+ local result1 = compute_ssl_verify({ schema = "https", host =
"127.0.0.1", port = "6443" })
+ ngx.say("https, no ssl_verify -> false: ", tostring(result1 ==
false))
+
+ -- Case 2: http with no ssl_verify set -> must be false
+ local result2 = compute_ssl_verify({ schema = "http", host =
"127.0.0.1", port = "6445" })
+ ngx.say("http, no ssl_verify -> false: ", tostring(result2 ==
false))
+
+ -- Case 3: explicit ssl_verify=true overrides default
+ local result3 = compute_ssl_verify({ schema = "https", host =
"127.0.0.1", port = "6443", ssl_verify = true })
+ ngx.say("explicit ssl_verify=true -> true: ", tostring(result3 ==
true))
+
+ -- Case 4: explicit ssl_verify=false is preserved
+ local result4 = compute_ssl_verify({ schema = "https", host =
"127.0.0.1", port = "6443", ssl_verify = false })
+ ngx.say("explicit ssl_verify=false -> false: ", tostring(result4
== false))
+ }
+ }
+--- response_body
+https, no ssl_verify -> false: true
+http, no ssl_verify -> false: true
+explicit ssl_verify=true -> true: true
+explicit ssl_verify=false -> false: true
diff --git a/t/kubernetes/discovery/kubernetes2.t
b/t/kubernetes/discovery/kubernetes2.t
index 9ec58f50c..41a3819d4 100644
--- a/t/kubernetes/discovery/kubernetes2.t
+++ b/t/kubernetes/discovery/kubernetes2.t
@@ -32,6 +32,7 @@ discovery:
service:
host: "127.0.0.1"
port: "6443"
+ ssl_verify: false
client:
token_file: "/tmp/var/run/secrets/kubernetes.io/serviceaccount/token"
- id: second
diff --git a/t/plugin/openid-connect2.t b/t/plugin/openid-connect2.t
index 44a4c63dc..e4760a11d 100644
--- a/t/plugin/openid-connect2.t
+++ b/t/plugin/openid-connect2.t
@@ -1001,3 +1001,83 @@ routes:
--- response_body
true
--- error_code: 302
+
+
+
+=== TEST 20: data encryption for client_rsa_private_key
+--- yaml_config
+apisix:
+ data_encryption:
+ enable: true
+ keyring:
+ - edd1c9f0985e76a2
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin").test
+ local rsa_key = "-----BEGIN RSA PRIVATE KEY-----\n" ..
+
"MIIEowIBAAKCAQEA0Z3VS5JJcds3xHn/ygWep4OHG5xbFGFiWXoXQWNe7mhZ6CJE\n" ..
+ "-----END RSA PRIVATE KEY-----"
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ json.encode({
+ plugins = {
+ ["openid-connect"] = {
+ client_id = "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
+ client_secret =
"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa",
+ client_rsa_private_key = rsa_key,
+ discovery =
"http://127.0.0.1:1980/.well-known/openid-configuration",
+ redirect_uri = "https://iresty.com",
+ ssl_verify = false,
+ timeout = 10,
+ scope = "apisix",
+ use_pkce = false,
+ session = {
+ secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK"
+ }
+ }
+ },
+ upstream = {
+ nodes = {
+ ["127.0.0.1:1980"] = 1
+ },
+ type = "roundrobin"
+ },
+ uri = "/hello"
+ })
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.sleep(0.1)
+
+ -- get plugin conf from admin api, key is decrypted
+ local code, message, res = t('/apisix/admin/routes/1',
+ ngx.HTTP_GET
+ )
+ res = json.decode(res)
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(message)
+ return
+ end
+
+ local plain_key =
res.value.plugins["openid-connect"].client_rsa_private_key
+ ngx.say(plain_key == rsa_key)
+
+ -- get plugin conf from etcd, key must be encrypted (not plaintext)
+ local etcd = require("apisix.core.etcd")
+ local etcd_res = assert(etcd.get('/routes/1'))
+ local stored =
etcd_res.body.node.value.plugins["openid-connect"].client_rsa_private_key
+ ngx.say(type(stored) == "string" and stored ~= "" and stored ~=
rsa_key)
+ }
+ }
+--- response_body
+true
+true
+--- no_error_log
+[alert]
diff --git a/t/router/radixtree-sni3.t b/t/router/radixtree-sni3.t
index ff18bda7f..d7cd40de6 100644
--- a/t/router/radixtree-sni3.t
+++ b/t/router/radixtree-sni3.t
@@ -281,3 +281,39 @@ server name: "www.test.com"
--- no_error_log
[error]
[alert]
+
+
+
+=== TEST 8: matched SSL object must not be logged (no private key leak via
debug log)
+--- config
+listen unix:$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+location /t {
+ content_by_lua_block {
+ -- etcd sync
+ ngx.sleep(0.2)
+ do
+ local sock = ngx.socket.tcp()
+ sock:settimeout(2000)
+ local ok, err =
sock:connect("unix:$TEST_NGINX_HTML_DIR/nginx.sock")
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+ local sess, err = sock:sslhandshake(nil, "www.test.com", false)
+ if not sess then
+ ngx.say("failed to do SSL handshake: ", err)
+ return
+ end
+ ngx.say("ssl handshake: ", sess ~= nil)
+ local ok, err = sock:close()
+ ngx.say("close: ", ok, " ", err)
+ end
+ }
+}
+--- request
+GET /t
+--- response_body
+ssl handshake: true
+close: 1 nil
+--- no_error_log
+debug - matched