Files
2026-07-13 12:32:21 +08:00

793 lines
23 KiB
Lua

local ssl_fixtures = require "spec.fixtures.ssl"
local helpers = require "spec.helpers"
local cjson = require "cjson"
local fmt = string.format
local function get_cert(server_name)
local _, _, stdout = assert(helpers.execute(
string.format("echo 'GET /' | openssl s_client -connect 0.0.0.0:%d -servername %s",
helpers.get_proxy_port(true), server_name)
))
return stdout
end
local fixtures = {}
local function reload_router(flavor)
helpers = require("spec.internal.module").reload_helpers(flavor)
fixtures.dns_mock = helpers.dns_mock.new({ mocks_only = true })
fixtures.dns_mock:A {
name = "example2.com",
address = "127.0.0.1",
}
end
-- TODO: remove it when we confirm it is not needed
local function gen_route(flavor, r)
return r
end
for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }) do
for _, strategy in helpers.each_strategy() do
describe("SSL [#" .. strategy .. ", flavor = " .. flavor .. "]", function()
local proxy_client
local https_client
reload_router(flavor)
lazy_setup(function()
local mock_tls_server_port = helpers.get_available_port()
fixtures.http_mock = {
test_upstream_tls_server = fmt([[
server {
server_name example2.com;
listen %s ssl;
ssl_certificate ../spec/fixtures/mtls_certs/example2.com.crt;
ssl_certificate_key ../spec/fixtures/mtls_certs/example2.com.key;
location = / {
echo 'it works';
}
}
]], mock_tls_server_port)
}
local bp = helpers.get_db_utils(strategy, {
"routes",
"services",
"certificates",
"snis",
})
local service = bp.services:insert {
name = "global-cert",
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "global.com" },
service = service,
}))
local service2 = bp.services:insert {
name = "api-1",
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "example.com", "ssl1.com" },
service = service2,
}))
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "sni.example.com" },
snis = { "sni.example.com" },
service = service2,
}))
local service4 = bp.services:insert {
name = "api-3",
protocol = helpers.mock_upstream_ssl_protocol,
host = helpers.mock_upstream_hostname,
port = helpers.mock_upstream_ssl_port,
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "ssl3.com" },
service = service4,
preserve_host = true,
}))
local service5 = bp.services:insert {
name = "api-4",
protocol = helpers.mock_upstream_ssl_protocol,
host = helpers.mock_upstream_hostname,
port = helpers.mock_upstream_ssl_port,
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "no-sni.com" },
service = service5,
preserve_host = false,
}))
local service6 = bp.services:insert {
name = "api-5",
protocol = helpers.mock_upstream_ssl_protocol,
host = "127.0.0.1",
port = helpers.mock_upstream_ssl_port,
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "nil-sni.com" },
service = service6,
preserve_host = false,
}))
local service7 = bp.services:insert {
name = "service-7",
protocol = helpers.mock_upstream_ssl_protocol,
host = helpers.mock_upstream_hostname,
port = helpers.mock_upstream_ssl_port,
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "example.com" },
paths = { "/redirect-301" },
https_redirect_status_code = 301,
service = service7,
preserve_host = false,
}))
local service8 = bp.services:insert {
name = "service-8",
protocol = helpers.mock_upstream_ssl_protocol,
host = helpers.mock_upstream_hostname,
port = helpers.mock_upstream_ssl_port,
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "example.com" },
paths = { "/redirect-302" },
https_redirect_status_code = 302,
service = service8,
preserve_host = false,
}))
local service_example2 = assert(bp.services:insert {
name = "service-example2",
protocol = "https",
host = "example2.com",
port = mock_tls_server_port,
})
assert(bp.routes:insert(gen_route(flavor, {
protocols = { "http" },
hosts = { "example2.com" },
paths = { "/" },
service = service_example2,
})))
assert(bp.routes:insert(gen_route(flavor, {
protocols = { "http" },
hosts = { "example-clear.com" },
paths = { "/" },
service = service8,
})))
local cert = bp.certificates:insert {
cert = ssl_fixtures.cert,
key = ssl_fixtures.key,
cert_alt = ssl_fixtures.cert_ecdsa,
key_alt = ssl_fixtures.key_ecdsa,
}
bp.snis:insert {
name = "example.com",
certificate = cert,
}
bp.snis:insert {
name = "ssl1.com",
certificate = cert,
}
-- wildcard tests
local certificate_alt = bp.certificates:insert {
cert = ssl_fixtures.cert_alt,
key = ssl_fixtures.key_alt,
}
local certificate_alt_alt = bp.certificates:insert {
cert = ssl_fixtures.cert_alt_alt,
key = ssl_fixtures.key_alt_alt,
cert_alt = ssl_fixtures.cert_alt_alt_ecdsa,
key_alt = ssl_fixtures.key_alt_alt_ecdsa,
}
bp.snis:insert {
name = "*.wildcard.com",
certificate = certificate_alt,
}
bp.snis:insert {
name = "wildcard.*",
certificate = certificate_alt,
}
bp.snis:insert {
name = "wildcard.org",
certificate = certificate_alt_alt,
}
bp.snis:insert {
name = "test.wildcard.*",
certificate = certificate_alt_alt,
}
bp.snis:insert {
name = "*.www.wildcard.com",
certificate = certificate_alt_alt,
}
-- /wildcard tests
assert(helpers.start_kong({
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
trusted_ips = "127.0.0.1",
nginx_http_proxy_ssl_verify = "on",
nginx_http_proxy_ssl_trusted_certificate = "../spec/fixtures/kong_spec.crt",
nginx_http_proxy_ssl_verify_depth = "5",
}, nil, nil, fixtures))
ngx.sleep(0.01)
proxy_client = helpers.proxy_client()
https_client = helpers.proxy_ssl_client()
end)
lazy_teardown(function()
helpers.stop_kong()
end)
describe("proxy ssl verify", function()
it("prevents requests to upstream that does not possess a trusted certificate", function()
helpers.clean_logfile()
local res = assert(proxy_client:send {
method = "GET",
path = "/",
headers = {
Host = "example2.com",
},
})
local body = assert.res_status(502, res)
assert.matches("An invalid response was received from the upstream server", body)
assert.logfile().has.line("upstream SSL certificate verify error: " ..
"(21:unable to verify the first certificate) " ..
"while SSL handshaking to upstream", true, 2)
end)
it("trusted certificate, request goes through", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/",
headers = {
Host = "example-clear.com",
}
})
assert.res_status(200, res)
end)
end)
describe("global SSL", function()
it("fallbacks on the default proxy SSL certificate when SNI is not provided by client", function()
local res = assert(https_client:send {
method = "GET",
path = "/status/200",
headers = {
Host = "global.com"
}
})
assert.res_status(200, res)
end)
end)
describe("handshake", function()
it("sets the default fallback SSL certificate if no SNI match", function()
local cert = get_cert("test.com")
assert.certificate(cert).has.cn("localhost")
end)
it("sets the configured SSL certificate if SNI match", function()
local cert = get_cert("ssl1.com")
assert.certificate(cert).has.cn("ssl-example.com")
cert = get_cert("example.com")
assert.certificate(cert).has.cn("ssl-example.com")
end)
describe("wildcard sni", function()
it("matches *.wildcard.com (prefix)", function()
local cert = get_cert("test.wildcard.com")
assert.matches("CN%s*=%s*ssl%-alt%.com", cert)
end)
it("matches wildcard.* (suffix)", function()
local cert = get_cert("wildcard.eu")
assert.matches("CN%s*=%s*ssl%-alt%.com", cert)
end)
it("respects matching priorities (exact first)", function()
local cert = get_cert("wildcard.org")
assert.matches("CN%s*=%s*ssl%-alt%-alt%.com", cert)
end)
it("respects matching priorities (prefix second)", function()
local cert = get_cert("test.wildcard.com")
assert.matches("CN%s*=%s*ssl%-alt%.com", cert)
end)
it("respects matching priorities (suffix third)", function()
local cert = get_cert("test.wildcard.org")
assert.matches("CN%s*=%s*ssl%-alt%-alt%.com", cert)
end)
it("matches *.www.wildcard.com", function()
local cert = get_cert("test.www.wildcard.com")
assert.matches("CN%s*=%s*ssl%-alt%-alt%.com", cert)
end)
end)
end)
describe("SSL termination", function()
it("blocks request without HTTPS if protocols = { http }", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/",
headers = {
["Host"] = "example.com",
}
})
local body = assert.res_status(426, res)
local json = cjson.decode(body)
assert.not_nil(json)
assert.matches("Please use HTTPS protocol", json.message)
assert.contains("Upgrade", res.headers.connection)
assert.equal("TLS/1.2, HTTP/1.1", res.headers.upgrade)
-- SNI case, see #6425
res = assert(proxy_client:send {
method = "GET",
path = "/",
headers = {
["Host"] = "sni.example.com",
}
})
body = assert.res_status(426, res)
json = cjson.decode(body)
assert.not_nil(json)
assert.matches("Please use HTTPS protocol", json.message)
assert.contains("Upgrade", res.headers.connection)
assert.equal("TLS/1.2, HTTP/1.1", res.headers.upgrade)
end)
it("returns 301 when route has https_redirect_status_code set to 301", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/redirect-301",
headers = {
["Host"] = "example.com",
}
})
assert.res_status(301, res)
assert.equal("https://example.com/redirect-301", res.headers.location)
end)
it("returns 302 when route has https_redirect_status_code set to 302", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/redirect-302?foo=bar",
headers = {
["Host"] = "example.com",
}
})
assert.res_status(302, res)
assert.equal("https://example.com/redirect-302?foo=bar", res.headers.location)
end)
describe("from not trusted_ip", function()
lazy_setup(function()
assert(helpers.restart_kong {
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
trusted_ips = nil,
})
proxy_client = helpers.proxy_client()
end)
it("blocks HTTP request with HTTPS in x-forwarded-proto", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
Host = "ssl1.com",
["x-forwarded-proto"] = "https"
}
})
assert.res_status(426, res)
end)
end)
describe("from trusted_ip", function()
lazy_setup(function()
assert(helpers.restart_kong {
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
trusted_ips = "127.0.0.1",
})
proxy_client = helpers.proxy_client()
end)
it("allows HTTP requests with x-forwarded-proto", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
Host = "example.com",
["x-forwarded-proto"] = "https",
}
})
assert.res_status(200, res)
end)
it("blocks HTTP requests with invalid x-forwarded-proto", function()
local res = assert(proxy_client:send {
method = "GET",
path = "/status/200",
headers = {
Host = "example.com",
["x-forwarded-proto"] = "httpsa"
}
})
assert.res_status(426, res)
end)
end)
describe("blocks with https x-forwarded-proto from untrusted client", function()
local client
-- restart kong and use a new client to simulate a connection from an
-- untrusted ip
lazy_setup(function()
assert(helpers.restart_kong {
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
trusted_ips = "1.2.3.4", -- explicitly trust an IP that is not us
})
client = helpers.proxy_client()
end)
-- despite reloading here with no trusted IPs, this
it("", function()
local res = assert(client:send {
method = "GET",
path = "/status/200",
headers = {
Host = "example.com",
["x-forwarded-proto"] = "https"
}
})
assert.res_status(426, res)
end)
end)
end)
describe("proxy_ssl_name", function()
local https_client_sni
before_each(function()
assert(helpers.restart_kong {
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
})
https_client_sni = helpers.proxy_ssl_client()
end)
after_each(function()
https_client_sni:close()
end)
describe("properly sets the upstream SNI with preserve_host", function()
it("true", function()
local res = assert(https_client_sni:send {
method = "GET",
path = "/",
headers = {
Host = "ssl3.com"
},
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("ssl3.com", json.vars.ssl_server_name)
end)
it("false", function()
local res = assert(https_client_sni:send {
method = "GET",
path = "/",
headers = {
Host = "no-sni.com"
},
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("localhost", json.vars.ssl_server_name)
end)
it("false and IP-based upstream_url", function()
local res = assert(https_client_sni:send {
method = "GET",
path = "/",
headers = {
Host = "nil-sni.com"
}
})
local body = assert.res_status(200, res)
local json = cjson.decode(body)
assert.equal("no SNI", json.vars.ssl_server_name)
end)
end)
end)
end)
describe("TLS proxy [#" .. strategy .. ", flavor = " .. flavor .. "]", function()
reload_router(flavor)
lazy_setup(function()
local bp = helpers.get_db_utils(strategy, {
"routes",
"services",
"certificates",
"snis",
})
local service = bp.services:insert {
name = "svc-http",
protocol = "tcp",
host = helpers.get_proxy_ip(),
port = helpers.get_proxy_port(),
}
bp.routes:insert(gen_route(flavor, {
protocols = { "tls" },
snis = { "example.com" },
service = service,
}))
bp.routes:insert(gen_route(flavor, {
protocols = { "tls" },
snis = { "foobar.example.com." },
service = service,
}))
local cert = bp.certificates:insert {
cert = ssl_fixtures.cert,
key = ssl_fixtures.key,
cert_alt = ssl_fixtures.cert_ecdsa,
key_alt = ssl_fixtures.key_ecdsa,
}
bp.snis:insert {
name = "example.com",
certificate = cert,
}
assert(helpers.start_kong {
router_flavor = flavor,
database = strategy,
stream_listen = "127.0.0.1:9020 ssl"
})
end)
lazy_teardown(function()
helpers.stop_kong()
end)
describe("can route normally", function()
it("sets the default certificate of '*' SNI", function()
local https_client = helpers.http_client({
scheme = "https",
host = "127.0.0.1",
port = 9020,
timeout = 60000,
ssl_verify = false,
ssl_server_name = "example.com",
})
local res = assert(https_client:send {
method = "GET",
path = "/",
})
assert.res_status(404, res)
local cert = get_cert("example.com")
-- this fails if the "example.com" SNI wasn't inserted above
assert.certificate(cert).has.cn("ssl-example.com")
https_client:close()
end)
it("using FQDN (regression for issue 7550)", function()
local https_client = helpers.http_client({
scheme = "https",
host = "127.0.0.1",
port = 9020,
timeout = 60000,
ssl_verify = false,
ssl_server_name = "foobar.example.com",
})
local res = assert(https_client:send {
method = "GET",
path = "/",
})
assert.res_status(404, res)
https_client:close()
end)
end)
end)
describe("SSL [#" .. strategy .. ", flavor = " .. flavor .. "]", function()
reload_router(flavor)
lazy_setup(function()
local bp = helpers.get_db_utils(strategy, {
"routes",
"services",
"certificates",
"snis",
})
local service = bp.services:insert {
name = "default-cert",
}
bp.routes:insert(gen_route(flavor, {
protocols = { "https" },
hosts = { "example.com" },
service = service,
}))
local cert = bp.certificates:insert {
cert = ssl_fixtures.cert,
key = ssl_fixtures.key,
cert_alt = ssl_fixtures.cert_ecdsa,
key_alt = ssl_fixtures.key_ecdsa,
}
bp.snis:insert {
name = "*",
certificate = cert,
}
assert(helpers.start_kong {
router_flavor = flavor,
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
})
end)
lazy_teardown(function()
helpers.stop_kong()
end)
describe("handshake", function()
it("sets the default certificate of '*' SNI", function()
local cert = get_cert("example.com")
assert.cn("ssl-example.com", cert)
end)
end)
end)
describe("kong.runloop.certificate invalid SNI [#" .. strategy .. ", flavor = " .. flavor .. "]", function()
reload_router(flavor)
lazy_setup(function()
assert(helpers.start_kong {
router_flavor = flavor,
database = strategy,
})
end)
lazy_teardown(function()
helpers.stop_kong()
end)
before_each(function()
helpers.clean_logfile()
end)
it("normal sni", function()
get_cert("a.example.com")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.no.line("invalid SNI", true)
end)
it("must not have a port", function()
get_cert("a.example.com:600")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI 'a.example.com:600', must not have a port", true)
end)
it("must not have a port (invalid port)", function()
get_cert("a.example.com:88888")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI 'a.example.com:88888', must not have a port", true)
end)
it("must not be an IP", function()
get_cert("127.0.0.1")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI '127.0.0.1', must not be an IP", true)
end)
it("must not be an IP (with port)", function()
get_cert("127.0.0.1:443")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI '127.0.0.1:443', must not be an IP", true)
end)
it("invalid value", function()
get_cert("256.256.256.256")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI '256.256.256.256', invalid value: ", true)
end)
it("only one wildcard must be specified", function()
get_cert("*.exam*le.com")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI '*.exam*le.com', only one wildcard must be specified", true)
end)
it("wildcard must be leftmost or rightmost character", function()
get_cert("a.exam*le.com")
assert.logfile().has.no.line("[error]", true)
assert.logfile().has.line("invalid SNI 'a.exam*le.com', wildcard must be leftmost or rightmost character", true)
end)
end)
end
end -- for flavor