Skip to content

Commit

Permalink
fix(templates): add CSP headers for Admin GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
sumimakito committed Feb 19, 2025
1 parent d5fcb42 commit 68ef6aa
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 1 deletion.
5 changes: 5 additions & 0 deletions changelog/unreleased/kong/feat-admin-gui-csp-header.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
message: >-
Kong Manager will now be served with the Content-Security-Policy (CSP) header by default. The CSP
header can be turned off with a new configuration parameter `admin_gui_csp_header`.
type: feature
scope: Core
7 changes: 7 additions & 0 deletions kong.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,13 @@
# use the window protocol + host and append the
# resolved admin_listen HTTP/HTTPS port.

#admin_gui_csp_header = on # Content-Security-Policy (CSP) header for Kong Manager
#
# This configuration parameter controls the presence of the
# `Content-Security-Policy` header while serving Kong Manager.
#
# Setting this parameter to `off` to turn off the CSP header.

#admin_gui_ssl_cert = # The SSL certificate for `admin_gui_listen` values
# with SSL enabled.
#
Expand Down
31 changes: 30 additions & 1 deletion kong/cmd/utils/prefix_handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ local log = require "kong.cmd.utils.log"
local ffi = require "ffi"
local bit = require "bit"
local nginx_signals = require "kong.cmd.utils.nginx_signals"
local admin_gui_utils = require "kong.admin_gui.utils"


local strip = require("kong.tools.string").strip
Expand Down Expand Up @@ -440,7 +441,35 @@ local function compile_kong_conf(kong_config, template_env_inject)
end

local function compile_kong_gui_include_conf(kong_config)
return compile_conf(kong_config, kong_nginx_gui_include_template)
-- Build connect-src in the CSP header
-- Other parts are defined inside nginx_kong_gui_include.lua
local csp_connect_src
if kong_config.admin_gui_csp_header then
-- TODO: Try bundling buttons.js with frontend assets instead of loading it from a URL
csp_connect_src = { "'self'", "https://api.github.com/repos/kong/kong" }
if kong_config.admin_gui_api_url then
table.insert(csp_connect_src, kong_config.admin_gui_api_url)
else
-- If admin_gui_api_url is missing, we will add dynamic sources that echoes the requested host
-- with ports defined in admin_listeners corresponding to the scheme
local api_listen = admin_gui_utils.select_listener(kong_config.admin_listeners, { ssl = false })
local api_port = api_listen and api_listen.port
if api_port then
table.insert(csp_connect_src, "http://$host:" .. api_port)
end

local api_ssl_listen = admin_gui_utils.select_listener(kong_config.admin_listeners, { ssl = true })
local api_ssl_port = api_ssl_listen and api_ssl_listen.port
if api_ssl_port then
table.insert(csp_connect_src, "https://$host:" .. api_ssl_port)
end
end
csp_connect_src = table.concat(csp_connect_src, " ")
end

return compile_conf(kong_config, kong_nginx_gui_include_template, {
csp_connect_src = csp_connect_src,
})
end

local function compile_kong_stream_conf(kong_config, template_env_inject)
Expand Down
1 change: 1 addition & 0 deletions kong/conf_loader/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ local CONF_PARSERS = {
admin_gui_url = { typ = "array" },
admin_gui_path = { typ = "string" },
admin_gui_api_url = { typ = "string" },
admin_gui_csp_header = { typ = "boolean" },

request_debug = { typ = "boolean" },
request_debug_token = { typ = "string" },
Expand Down
1 change: 1 addition & 0 deletions kong/templates/kong_defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ untrusted_lua_sandbox_environment =
admin_gui_url =
admin_gui_path = /
admin_gui_api_url = NONE
admin_gui_csp_header = on
openresty_path =
Expand Down
8 changes: 8 additions & 0 deletions kong/templates/nginx_kong_gui_include.lua
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ location ~* ^$(admin_gui_path_prefix)(?<path>/.*)?$ {
gzip_types text/plain text/css application/json application/javascript;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
> if csp_connect_src then
> -- [CSP] 'wasm-unsafe-eval' in script-src is required for atc-router-wasm
> -- [CSP] TODO: 'unsafe-inline' is still required for style-src because of monaco-editor. See: https://github.com/microsoft/monaco-editor/issues/271
add_header Content-Security-Policy "default-src 'self'; connect-src $(csp_connect_src); img-src 'self' data:; script-src 'self' 'wasm-unsafe-eval'; script-src-elem 'self' https://buttons.github.io/buttons.js; style-src 'self' 'unsafe-inline';";
> end
add_header Referrer-Policy 'strict-origin-when-cross-origin';
add_header X-Frame-Options 'sameorigin';
add_header X-XSS-Protection '1; mode=block';
add_header X-Content-Type-Options 'nosniff';
Expand Down
1 change: 1 addition & 0 deletions spec/01-unit/03-conf_loader_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe("Configuration loader", function()
assert.same({"0.0.0.0:8000 reuseport backlog=16384", "0.0.0.0:8443 http2 ssl reuseport backlog=16384"}, conf.proxy_listen)
assert.same({"0.0.0.0:8002", "0.0.0.0:8445 ssl"}, conf.admin_gui_listen)
assert.equal("/", conf.admin_gui_path)
assert.equal(true, conf.admin_gui_csp_header)
assert.equal("logs/admin_gui_access.log", conf.admin_gui_access_log)
assert.equal("logs/admin_gui_error.log", conf.admin_gui_error_log)
assert.same({}, conf.ssl_cert) -- check placeholder value
Expand Down
111 changes: 111 additions & 0 deletions spec/01-unit/04-prefix_handler_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1613,4 +1613,115 @@ describe("NGINX conf compiler", function()
assert.matches("include 'nginx-kong-stream-inject.conf';", nginx_conf, nil, true)
end)
end)

describe("compile_kong_gui_include_conf()", function()
describe("Content-Security-Policy", function()
it("should add header by default with default admin_listen", function()
local conf = assert(conf_loader(helpers.test_conf_path))
local gui_include_conf = assert(prefix_handler.compile_kong_gui_include_conf(conf))
local found_connect_src = false

for line in gui_include_conf:gmatch("(.-)\n") do
if line:find("add_header Content-Security-Policy", 1, true) then
assert.matches("connect-src 'self' https://api.github.com/repos/kong/kong http://$host:9001;", line, nil,
true)
found_connect_src = true
break
end
end

assert.True(found_connect_src)
end)

it("should add header with one more secure admin_listen", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
admin_listen = "127.0.0.1:9001, 127.0.0.1:9444 ssl"
}))
local gui_include_conf = assert(prefix_handler.compile_kong_gui_include_conf(conf))
local found_connect_src = false

for line in gui_include_conf:gmatch("(.-)\n") do
if line:find("add_header Content-Security-Policy", 1, true) then
assert.matches(
"connect-src 'self' https://api.github.com/repos/kong/kong http://$host:9001 https://$host:9444;", line, nil,
true)
found_connect_src = true
break
end
end

assert.True(found_connect_src)
end)

it("should add header with only secure admin_listen", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
admin_listen = "127.0.0.1:9444 ssl"
}))
local gui_include_conf = assert(prefix_handler.compile_kong_gui_include_conf(conf))
local found_connect_src = false

for line in gui_include_conf:gmatch("(.-)\n") do
if line:find("add_header Content-Security-Policy", 1, true) then
assert.matches(
"connect-src 'self' https://api.github.com/repos/kong/kong https://$host:9444;", line, nil,
true)
found_connect_src = true
break
end
end

assert.True(found_connect_src)
end)

it("should add header without admin_listen", function()
-- Although kong_gui is not served when admin_listeners is off, we are test against the
-- compile function itself.
local conf = assert(conf_loader(helpers.test_conf_path, {
admin_listen = "off"
}))
local gui_include_conf = assert(prefix_handler.compile_kong_gui_include_conf(conf))
local found_connect_src = false

for line in gui_include_conf:gmatch("(.-)\n") do
if line:find("add_header Content-Security-Policy", 1, true) then
assert.matches(
"connect-src 'self' https://api.github.com/repos/kong/kong;", line, nil, true)
found_connect_src = true
break
end
end

assert.True(found_connect_src)
end)

it("should add header with custom admin_gui_api_url", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
admin_gui_api_url = "http://admin-api.kong.local:18001"
}))
local gui_include_conf = assert(prefix_handler.compile_kong_gui_include_conf(conf))
local found_connect_src = false

for line in gui_include_conf:gmatch("(.-)\n") do
if line:find("add_header Content-Security-Policy", 1, true) then
assert.matches(
"connect-src 'self' https://api.github.com/repos/kong/kong http://admin-api.kong.local:18001;", line, nil,
true)
found_connect_src = true
break
end
end

assert.True(found_connect_src)
end)

it("should not add header when turned off", function()
local conf = assert(conf_loader(helpers.test_conf_path, {
admin_gui_csp_header = "off",
}))
local gui_include_conf = prefix_handler.compile_kong_gui_include_conf(conf)

assert.not_matches("add_header Content-Security-Policy", gui_include_conf, nil, true)
end)
end)
end)
end)

0 comments on commit 68ef6aa

Please sign in to comment.