diff --git a/conf/defaults.ini b/conf/defaults.ini
index 115cbc20a73..1892554a067 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -85,6 +85,11 @@ cdn_url =
# `0` means there is no timeout for reading the request.
read_timeout = 0
+# This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
+[server.custom_response_headers]
+#exampleHeader1 = exampleValue1
+#exampleHeader2 = exampleValue2
+
#################################### Database ############################
[database]
# You can configure the database connection by specifying type, host, name, user and password
diff --git a/conf/sample.ini b/conf/sample.ini
index d421d04c10a..8f880bd1ebc 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -86,6 +86,11 @@
# `0` means there is no timeout for reading the request.
;read_timeout = 0
+# This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
+[server.custom_response_headers]
+#exampleHeader1 = exampleValue1
+#exampleHeader2 = exampleValue2
+
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md
index 542f006729f..a258f2ab5b7 100644
--- a/docs/sources/setup-grafana/configure-grafana/_index.md
+++ b/docs/sources/setup-grafana/configure-grafana/_index.md
@@ -292,6 +292,17 @@ Sets the maximum time using a duration format (5s/5m/5ms) before timing out read
+## [server.custom_response_headers]
+
+This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
+
+```
+exampleHeader1 = exampleValue1
+exampleHeader2 = exampleValue2
+```
+
+
+
## [database]
Grafana needs a database to store users and dashboards (and other
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index ed99aa51c94..ae383a02732 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -598,6 +598,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
}
+ if len(hs.Cfg.CustomResponseHeaders) > 0 {
+ m.Use(middleware.AddCustomResponseHeaders(hs.Cfg))
+ }
+
m.Use(middleware.AddDefaultResponseHeaders(hs.Cfg))
if hs.Cfg.ServeFromSubPath && hs.Cfg.AppSubURL != "" {
diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go
index 22225cecd75..940fd5e36c1 100644
--- a/pkg/middleware/middleware.go
+++ b/pkg/middleware/middleware.go
@@ -77,3 +77,21 @@ func addNoCacheHeaders(w web.ResponseWriter) {
func addXFrameOptionsDenyHeader(w web.ResponseWriter) {
w.Header().Set("X-Frame-Options", "deny")
}
+
+func AddCustomResponseHeaders(cfg *setting.Cfg) web.Handler {
+ return func(c *web.Context) {
+ c.Resp.Before(func(w web.ResponseWriter) {
+ if w.Written() {
+ return
+ }
+
+ for header, value := range cfg.CustomResponseHeaders {
+ // do not override existing headers
+ if w.Header().Get(header) != "" {
+ continue
+ }
+ w.Header().Set(header, value)
+ }
+ })
+ }
+}
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index 8c029c2f597..1c45065c374 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -514,6 +514,17 @@ func TestMiddlewareContext(t *testing.T) {
cfg.AnonymousOrgRole = string(org.RoleEditor)
})
+ middlewareScenario(t, "middleware should add custom response headers", func(t *testing.T, sc *scenarioContext) {
+ sc.fakeReq("GET", "/api/").exec()
+ assert.Regexp(t, "test", sc.resp.Header().Get("X-Custom-Header"))
+ assert.Regexp(t, "other-test", sc.resp.Header().Get("X-Other-Header"))
+ }, func(cfg *setting.Cfg) {
+ cfg.CustomResponseHeaders = map[string]string{
+ "X-Custom-Header": "test",
+ "X-Other-Header": "other-test",
+ }
+ })
+
t.Run("auth_proxy", func(t *testing.T) {
const userID int64 = 33
const orgID int64 = 4
@@ -811,6 +822,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
require.Truef(t, exists, "Views directory should exist at %q", viewsPath)
sc.m = web.New()
+ sc.m.Use(AddCustomResponseHeaders(cfg))
sc.m.Use(AddDefaultResponseHeaders(cfg))
sc.m.UseMiddleware(ContentSecurityPolicy(cfg, logger))
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index 70372fcd9f3..bc993a3e07d 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -486,6 +486,8 @@ type Cfg struct {
GRPCServerNetwork string
GRPCServerAddress string
GRPCServerTLSConfig *tls.Config
+
+ CustomResponseHeaders map[string]string
}
type CommandLineArgs struct {
@@ -1692,6 +1694,14 @@ func (cfg *Cfg) readServerSettings(iniFile *ini.File) error {
cfg.ReadTimeout = server.Key("read_timeout").MustDuration(0)
+ headersSection := cfg.Raw.Section("server.custom_response_headers")
+ keys := headersSection.Keys()
+ cfg.CustomResponseHeaders = make(map[string]string, len(keys))
+
+ for _, key := range keys {
+ cfg.CustomResponseHeaders[key.Name()] = key.Value()
+ }
+
return nil
}