Skip to content
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 apisix/discovery/kubernetes/informer_factory.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apisix/discovery/kubernetes/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions apisix/discovery/kubernetes/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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" }
},
Expand Down
2 changes: 0 additions & 2 deletions apisix/ssl/router/radixtree_sni.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
217 changes: 217 additions & 0 deletions t/discovery/kubernetes_ssl_verify.t
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions t/kubernetes/discovery/kubernetes2.t
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions t/plugin/openid-connect2.t
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading
Loading