diff --git a/pkg/util/proxyutil/proxyutil.go b/pkg/util/proxyutil/proxyutil.go index 68ae725919a..6cccf55e37d 100644 --- a/pkg/util/proxyutil/proxyutil.go +++ b/pkg/util/proxyutil/proxyutil.go @@ -1,6 +1,7 @@ package proxyutil import ( + "fmt" "net" "net/http" "sort" @@ -75,6 +76,16 @@ func SetProxyResponseHeaders(header http.Header) { header.Set("Content-Security-Policy", "sandbox") } +// SetViaHeader adds Grafana's reverse proxy to the proxy chain. +// Defined in RFC 9110 7.6.3 https://datatracker.ietf.org/doc/html/rfc9110#name-via +func SetViaHeader(header http.Header, major, minor int) { + via := fmt.Sprintf("%d.%d grafana", major, minor) + if old := header.Get("Via"); old != "" { + via = fmt.Sprintf("%s, %s", via, old) + } + header.Set("Via", via) +} + // ApplyUserHeader Set the X-Grafana-User header if needed (and remove if not). func ApplyUserHeader(sendUserHeader bool, req *http.Request, user *user.SignedInUser) { req.Header.Del(UserHeaderName) diff --git a/pkg/util/proxyutil/reverse_proxy.go b/pkg/util/proxyutil/reverse_proxy.go index 668015edf15..fd3c8ea95ec 100644 --- a/pkg/util/proxyutil/reverse_proxy.go +++ b/pkg/util/proxyutil/reverse_proxy.go @@ -79,11 +79,30 @@ func wrapDirector(d func(*http.Request)) func(req *http.Request) { } } +// deletedHeaders lists a number of headers that we don't want to +// pass-through from the upstream when using a reverse proxy. +// +// These are related to the connection between Grafana and the proxy +// or instructions that would alter how a browser will interact with +// future requests to Grafana (such as enabling Strict Transport +// Security) +var deletedHeaders = []string{ + "Alt-Svc", + "Close", + "Server", + "Set-Cookie", + "Strict-Transport-Security", +} + // modifyResponse enforces certain constraints on http.Response. func modifyResponse(logger glog.Logger) func(resp *http.Response) error { return func(resp *http.Response) error { - resp.Header.Del("Set-Cookie") + for _, header := range deletedHeaders { + resp.Header.Del(header) + } + SetProxyResponseHeaders(resp.Header) + SetViaHeader(resp.Header, resp.ProtoMajor, resp.ProtoMinor) return nil } } diff --git a/pkg/util/proxyutil/reverse_proxy_test.go b/pkg/util/proxyutil/reverse_proxy_test.go index 602575d660f..ada4609f981 100644 --- a/pkg/util/proxyutil/reverse_proxy_test.go +++ b/pkg/util/proxyutil/reverse_proxy_test.go @@ -21,6 +21,8 @@ func TestReverseProxy(t *testing.T) { upstream := newUpstreamServer(t, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { actualReq = req http.SetCookie(w, &http.Cookie{Name: "test"}) + w.Header().Set("Strict-Transport-Security", "max-age=31536000") + w.Header().Set("X-Custom-Hdr", "Ok!") w.WriteHeader(http.StatusOK) })) t.Cleanup(upstream.Close) @@ -52,11 +54,14 @@ func TestReverseProxy(t *testing.T) { require.Empty(t, actualReq.Header.Get("Referer")) require.Equal(t, "https://test.com/api", actualReq.Header.Get("X-Grafana-Referer")) require.Equal(t, "value", actualReq.Header.Get("X-KEY")) + require.Empty(t, actualReq.Header.Get("Authorization")) resp := rec.Result() require.Empty(t, resp.Cookies()) require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy")) + require.Contains(t, resp.Header, "X-Custom-Hdr") + require.NotContains(t, resp.Header, "Strict-Transport-Security") + require.Contains(t, resp.Header.Get("Via"), "grafana") require.NoError(t, resp.Body.Close()) - require.Empty(t, actualReq.Header.Get("Authorization")) }) t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {