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