Skip to content

Commit 2e0b6b2

Browse files
authored
fix: address TLS security vulnerabilities in SSL log, OIDC encryption, and K8s ssl_verify (#13190)
1 parent d212a81 commit 2e0b6b2

File tree

8 files changed

+354
-3
lines changed

8 files changed

+354
-3
lines changed

apisix/discovery/kubernetes/informer_factory.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ local function list_watch(informer, apiserver)
273273
scheme = apiserver.schema,
274274
host = apiserver.host,
275275
port = apiserver.port,
276-
ssl_verify = false
276+
ssl_verify = apiserver.ssl_verify
277277
})
278278

279279
if not ok then

apisix/discovery/kubernetes/init.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,13 @@ local function get_apiserver(conf)
479479
return nil, "apiserver.token should set to non-empty string when service.schema is https"
480480
end
481481

482+
-- ssl_verify: use explicit config if set, otherwise default to false
483+
if conf.service.ssl_verify ~= nil then
484+
apiserver.ssl_verify = conf.service.ssl_verify
485+
else
486+
apiserver.ssl_verify = false
487+
end
488+
482489
return apiserver
483490
end
484491

apisix/discovery/kubernetes/schema.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ return {
129129
oneOf = port_patterns,
130130
default = "${KUBERNETES_SERVICE_PORT}",
131131
},
132+
ssl_verify = {
133+
type = "boolean",
134+
description = "Verify the TLS certificate of the Kubernetes API " ..
135+
"server. Defaults to false. Set to true to enable " ..
136+
"certificate verification.",
137+
},
132138
},
133139
default = {
134140
schema = "https",
@@ -190,6 +196,12 @@ return {
190196
type = "string",
191197
oneOf = port_patterns,
192198
},
199+
ssl_verify = {
200+
type = "boolean",
201+
description = "Verify the TLS certificate of the Kubernetes " ..
202+
"API server. Defaults to false. Set to true to " ..
203+
"enable certificate verification.",
204+
},
193205
},
194206
required = { "host", "port" }
195207
},

apisix/ssl/router/radixtree_sni.lua

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,6 @@ function _M.match_and_set(api_ctx, match_only, alt_sni)
214214
end
215215
end
216216

217-
core.log.info("debug - matched: ", core.json.delay_encode(api_ctx.matched_ssl, true))
218-
219217
if match_only then
220218
return true
221219
end
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
use t::APISIX 'no_plan';
19+
20+
repeat_each(1);
21+
no_long_string();
22+
no_shuffle();
23+
no_root_location();
24+
log_level("info");
25+
26+
add_block_preprocessor(sub {
27+
my ($block) = @_;
28+
29+
if (!$block->request) {
30+
$block->set_value("request", "GET /t");
31+
}
32+
33+
if (!$block->error_log && !$block->no_error_log) {
34+
$block->set_value("no_error_log", "[error]\n[alert]");
35+
}
36+
});
37+
38+
run_tests;
39+
40+
__DATA__
41+
42+
=== TEST 1: ssl_verify is false when apiserver uses http scheme (no explicit config)
43+
--- config
44+
location /t {
45+
content_by_lua_block {
46+
local captured_opts = {}
47+
48+
-- Monkeypatch resty.http to capture connect options
49+
local http_orig = require("resty.http")
50+
local http_new_orig = http_orig.new
51+
http_orig.new = function()
52+
local mock = {}
53+
mock.connect = function(self, opts)
54+
captured_opts.ssl_verify = opts and opts.ssl_verify
55+
-- simulate connection refused to stop further processing
56+
return false, "connection refused"
57+
end
58+
return mock
59+
end
60+
61+
local factory = require("apisix.discovery.kubernetes.informer_factory")
62+
local informer = factory.new(nil, "v1", "Endpoints", "endpoints", nil)
63+
64+
-- apiserver.ssl_verify is set by init.lua get_apiserver; simulate http result
65+
local apiserver = {
66+
schema = "http",
67+
host = "127.0.0.1",
68+
port = 6445,
69+
ssl_verify = false, -- what get_apiserver would set for http scheme
70+
}
71+
72+
informer:list_watch(apiserver)
73+
ngx.say("ssl_verify for http scheme: ", tostring(captured_opts.ssl_verify))
74+
75+
-- restore
76+
http_orig.new = http_new_orig
77+
}
78+
}
79+
--- response_body
80+
ssl_verify for http scheme: false
81+
--- no_error_log
82+
[alert]
83+
84+
85+
86+
=== TEST 2: ssl_verify defaults to false when apiserver uses https scheme (no explicit config)
87+
--- config
88+
location /t {
89+
content_by_lua_block {
90+
local captured_opts = {}
91+
92+
-- Monkeypatch resty.http to capture connect options
93+
local http_orig = require("resty.http")
94+
local http_new_orig = http_orig.new
95+
http_orig.new = function()
96+
local mock = {}
97+
mock.connect = function(self, opts)
98+
captured_opts.ssl_verify = opts and opts.ssl_verify
99+
-- simulate connection refused to stop further processing
100+
return false, "connection refused"
101+
end
102+
return mock
103+
end
104+
105+
local factory = require("apisix.discovery.kubernetes.informer_factory")
106+
local informer = factory.new(nil, "v1", "Endpoints", "endpoints", nil)
107+
108+
-- apiserver.ssl_verify is set by init.lua get_apiserver; default is false
109+
local apiserver = {
110+
schema = "https",
111+
host = "127.0.0.1",
112+
port = 6443,
113+
ssl_verify = false, -- default when no explicit config, even for https
114+
}
115+
116+
informer:list_watch(apiserver)
117+
ngx.say("ssl_verify for https scheme (no config): ", tostring(captured_opts.ssl_verify))
118+
119+
-- restore
120+
http_orig.new = http_new_orig
121+
}
122+
}
123+
--- response_body
124+
ssl_verify for https scheme (no config): false
125+
--- no_error_log
126+
[alert]
127+
128+
129+
130+
=== TEST 3: explicit ssl_verify=true enables certificate verification for https
131+
--- config
132+
location /t {
133+
content_by_lua_block {
134+
local captured_opts = {}
135+
136+
-- Monkeypatch resty.http to capture connect options
137+
local http_orig = require("resty.http")
138+
local http_new_orig = http_orig.new
139+
http_orig.new = function()
140+
local mock = {}
141+
mock.connect = function(self, opts)
142+
captured_opts.ssl_verify = opts and opts.ssl_verify
143+
return false, "connection refused"
144+
end
145+
return mock
146+
end
147+
148+
local factory = require("apisix.discovery.kubernetes.informer_factory")
149+
local informer = factory.new(nil, "v1", "Endpoints", "endpoints", nil)
150+
151+
-- Simulate get_apiserver with explicit ssl_verify=true (user opts in)
152+
local apiserver = {
153+
schema = "https",
154+
host = "127.0.0.1",
155+
port = 6443,
156+
ssl_verify = true, -- explicit opt-in for certificate verification
157+
}
158+
159+
informer:list_watch(apiserver)
160+
ngx.say("explicit ssl_verify=true respected: ", tostring(captured_opts.ssl_verify == true))
161+
162+
-- restore
163+
http_orig.new = http_new_orig
164+
}
165+
}
166+
--- response_body
167+
explicit ssl_verify=true respected: true
168+
--- no_error_log
169+
[alert]
170+
171+
172+
173+
=== TEST 4: get_apiserver defaults ssl_verify to false when service.ssl_verify is not configured
174+
--- config
175+
location /t {
176+
content_by_lua_block {
177+
-- Verify the contract of get_apiserver() in init.lua:
178+
-- when conf.service.ssl_verify is nil (not configured), ssl_verify must be false.
179+
-- This is the backward-compatible default — NOT derived from the scheme.
180+
--
181+
-- The logic being tested (init.lua):
182+
-- if conf.service.ssl_verify ~= nil then
183+
-- apiserver.ssl_verify = conf.service.ssl_verify
184+
-- else
185+
-- apiserver.ssl_verify = false <-- must be false, not (schema == "https")
186+
-- end
187+
188+
local function compute_ssl_verify(service_conf)
189+
if service_conf.ssl_verify ~= nil then
190+
return service_conf.ssl_verify
191+
else
192+
return false
193+
end
194+
end
195+
196+
-- Case 1: https with no ssl_verify set -> must be false
197+
local result1 = compute_ssl_verify({ schema = "https", host = "127.0.0.1", port = "6443" })
198+
ngx.say("https, no ssl_verify -> false: ", tostring(result1 == false))
199+
200+
-- Case 2: http with no ssl_verify set -> must be false
201+
local result2 = compute_ssl_verify({ schema = "http", host = "127.0.0.1", port = "6445" })
202+
ngx.say("http, no ssl_verify -> false: ", tostring(result2 == false))
203+
204+
-- Case 3: explicit ssl_verify=true overrides default
205+
local result3 = compute_ssl_verify({ schema = "https", host = "127.0.0.1", port = "6443", ssl_verify = true })
206+
ngx.say("explicit ssl_verify=true -> true: ", tostring(result3 == true))
207+
208+
-- Case 4: explicit ssl_verify=false is preserved
209+
local result4 = compute_ssl_verify({ schema = "https", host = "127.0.0.1", port = "6443", ssl_verify = false })
210+
ngx.say("explicit ssl_verify=false -> false: ", tostring(result4 == false))
211+
}
212+
}
213+
--- response_body
214+
https, no ssl_verify -> false: true
215+
http, no ssl_verify -> false: true
216+
explicit ssl_verify=true -> true: true
217+
explicit ssl_verify=false -> false: true

t/kubernetes/discovery/kubernetes2.t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ discovery:
3232
service:
3333
host: "127.0.0.1"
3434
port: "6443"
35+
ssl_verify: false
3536
client:
3637
token_file: "/tmp/var/run/secrets/kubernetes.io/serviceaccount/token"
3738
- id: second

t/plugin/openid-connect2.t

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,3 +1001,83 @@ routes:
10011001
--- response_body
10021002
true
10031003
--- error_code: 302
1004+
1005+
1006+
1007+
=== TEST 20: data encryption for client_rsa_private_key
1008+
--- yaml_config
1009+
apisix:
1010+
data_encryption:
1011+
enable: true
1012+
keyring:
1013+
- edd1c9f0985e76a2
1014+
--- config
1015+
location /t {
1016+
content_by_lua_block {
1017+
local json = require("toolkit.json")
1018+
local t = require("lib.test_admin").test
1019+
local rsa_key = "-----BEGIN RSA PRIVATE KEY-----\n" ..
1020+
"MIIEowIBAAKCAQEA0Z3VS5JJcds3xHn/ygWep4OHG5xbFGFiWXoXQWNe7mhZ6CJE\n" ..
1021+
"-----END RSA PRIVATE KEY-----"
1022+
local code, body = t('/apisix/admin/routes/1',
1023+
ngx.HTTP_PUT,
1024+
json.encode({
1025+
plugins = {
1026+
["openid-connect"] = {
1027+
client_id = "kbyuFDidLLm280LIwVFiazOqjO3ty8KH",
1028+
client_secret = "60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa",
1029+
client_rsa_private_key = rsa_key,
1030+
discovery = "http://127.0.0.1:1980/.well-known/openid-configuration",
1031+
redirect_uri = "https://iresty.com",
1032+
ssl_verify = false,
1033+
timeout = 10,
1034+
scope = "apisix",
1035+
use_pkce = false,
1036+
session = {
1037+
secret = "jwcE5v3pM9VhqLxmxFOH9uZaLo8u7KQK"
1038+
}
1039+
}
1040+
},
1041+
upstream = {
1042+
nodes = {
1043+
["127.0.0.1:1980"] = 1
1044+
},
1045+
type = "roundrobin"
1046+
},
1047+
uri = "/hello"
1048+
})
1049+
)
1050+
1051+
if code >= 300 then
1052+
ngx.status = code
1053+
ngx.say(body)
1054+
return
1055+
end
1056+
ngx.sleep(0.1)
1057+
1058+
-- get plugin conf from admin api, key is decrypted
1059+
local code, message, res = t('/apisix/admin/routes/1',
1060+
ngx.HTTP_GET
1061+
)
1062+
res = json.decode(res)
1063+
if code >= 300 then
1064+
ngx.status = code
1065+
ngx.say(message)
1066+
return
1067+
end
1068+
1069+
local plain_key = res.value.plugins["openid-connect"].client_rsa_private_key
1070+
ngx.say(plain_key == rsa_key)
1071+
1072+
-- get plugin conf from etcd, key must be encrypted (not plaintext)
1073+
local etcd = require("apisix.core.etcd")
1074+
local etcd_res = assert(etcd.get('/routes/1'))
1075+
local stored = etcd_res.body.node.value.plugins["openid-connect"].client_rsa_private_key
1076+
ngx.say(type(stored) == "string" and stored ~= "" and stored ~= rsa_key)
1077+
}
1078+
}
1079+
--- response_body
1080+
true
1081+
true
1082+
--- no_error_log
1083+
[alert]

0 commit comments

Comments
 (0)