diff --git a/Gopkg.lock b/Gopkg.lock index a5af15a42..443b5b71f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1554,11 +1554,11 @@ [[projects]] branch = "v1" - digest = "1:60888cead16f066c948c078258b27f2885dce91cb6aadacf545b62a1ae1d08cb" + digest = "1:ca2fa30e83e743bf24f13fbfa883e228955748ab59785b6ae9bd8a55a05bd157" name = "github.com/unrolled/secure" packages = ["."] pruneopts = "NUT" - revision = "a1cf62cc2159fff407728f118c41aece76c397fa" + revision = "716474489ad3d350e961ec6de19a6ab24e40f006" [[projects]] digest = "1:a68c3f55d44d225da4f22ffbed2d8572d267cb19aaa1d60537769034ac66bc01" @@ -2239,6 +2239,7 @@ "github.com/docker/go-connections/sockets", "github.com/eapache/channels", "github.com/elazarl/go-bindata-assetfs", + "github.com/fatih/structs", "github.com/gambol99/go-marathon", "github.com/go-acme/lego/certcrypto", "github.com/go-acme/lego/certificate", diff --git a/docs/content/middlewares/headers.md b/docs/content/middlewares/headers.md index 47b1e0305..a81be1a52 100644 --- a/docs/content/middlewares/headers.md +++ b/docs/content/middlewares/headers.md @@ -370,6 +370,10 @@ The `publicKey` implements HPKP to prevent MITM attacks with forged certificates The `referrerPolicy` allows sites to control when browsers will pass the Referer header to other sites. +### `featurePolicy` + +The `featurePolicy` allows sites to control browser features. + ### `isDevelopment` Set `isDevelopment` to true when developing. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 29541203f..bde9e1c2c 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -51,6 +51,7 @@ - "traefik.http.middlewares.middleware09.headers.isdevelopment=true" - "traefik.http.middlewares.middleware09.headers.publickey=foobar" - "traefik.http.middlewares.middleware09.headers.referrerpolicy=foobar" +- "traefik.http.middlewares.middleware09.headers.featurepolicy=foobar" - "traefik.http.middlewares.middleware09.headers.sslforcehost=true" - "traefik.http.middlewares.middleware09.headers.sslhost=foobar" - "traefik.http.middlewares.middleware09.headers.sslproxyheaders.name0=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 033fe61e4..1bc9c258c 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -161,6 +161,7 @@ contentSecurityPolicy = "foobar" publicKey = "foobar" referrerPolicy = "foobar" + featurePolicy = "foobar" isDevelopment = true [http.middlewares.Middleware09.headers.customRequestHeaders] name0 = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index a3e1d9b21..a789b6d0d 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -194,6 +194,7 @@ http: contentSecurityPolicy: foobar publicKey: foobar referrerPolicy: foobar + featurePolicy: foobar isDevelopment: true Middleware10: ipWhiteList: diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 45570105b..a3419d75e 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -51,6 +51,7 @@ "traefik.http.middlewares.middleware09.headers.isdevelopment": "true", "traefik.http.middlewares.middleware09.headers.publickey": "foobar", "traefik.http.middlewares.middleware09.headers.referrerpolicy": "foobar", +"traefik.http.middlewares.middleware09.headers.featurepolicy": "foobar", "traefik.http.middlewares.middleware09.headers.sslforcehost": "true", "traefik.http.middlewares.middleware09.headers.sslhost": "foobar", "traefik.http.middlewares.middleware09.headers.sslproxyheaders.name0": "foobar", diff --git a/integration/fixtures/headers/secure.toml b/integration/fixtures/headers/secure.toml new file mode 100644 index 000000000..d04e5a9c8 --- /dev/null +++ b/integration/fixtures/headers/secure.toml @@ -0,0 +1,34 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + rule = "Host(`test.localhost`)" + middlewares = ["secure"] + service = "service1" + + [http.routers.router2] + rule = "Host(`test2.localhost`)" + service = "service1" + +[http.middlewares] + [http.middlewares.secure.headers] + featurePolicy = "vibrate 'none';" + +[http.services] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9000" diff --git a/integration/headers_test.go b/integration/headers_test.go index aa4fd2653..7e0a1422e 100644 --- a/integration/headers_test.go +++ b/integration/headers_test.go @@ -113,3 +113,43 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) { c.Assert(err, checker.IsNil) } } + +func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) { + file := s.adaptFile(c, "fixtures/headers/secure.toml", struct{}{}) + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + backend := startTestServer("9000", http.StatusOK) + defer backend.Close() + + err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) + + testCase := []struct { + desc string + expected http.Header + reqHost string + }{ + { + desc: "Feature-Policy Set", + expected: http.Header{ + "Feature-Policy": {"vibrate 'none';"}, + }, + reqHost: "test.localhost", + }, + } + + for _, test := range testCase { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = test.reqHost + + err = try.Request(req, 500*time.Millisecond, try.HasHeaderStruct(test.expected)) + c.Assert(err, checker.IsNil) + } +} diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index e177d32bf..017d1db0d 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -384,6 +384,7 @@ contentSecurityPolicy = "foobar" publicKey = "foobar" referrerPolicy = "foobar" + featurePolicy = "foobar" isDevelopment = true [http.middlewares.Middleware8.headers.customRequestHeaders] name0 = "foobar" @@ -476,4 +477,4 @@ [tls.stores.Store1] [tls.stores.Store1.defaultCertificate] certFile = "foobar" - keyFile = "foobar" \ No newline at end of file + keyFile = "foobar" diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index b5124a822..9d8520e07 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -167,6 +167,7 @@ type Headers struct { ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty" toml:"contentSecurityPolicy,omitempty" yaml:"contentSecurityPolicy,omitempty"` PublicKey string `json:"publicKey,omitempty" toml:"publicKey,omitempty" yaml:"publicKey,omitempty"` ReferrerPolicy string `json:"referrerPolicy,omitempty" toml:"referrerPolicy,omitempty" yaml:"referrerPolicy,omitempty"` + FeaturePolicy string `json:"featurePolicy,omitempty" toml:"featurePolicy,omitempty" yaml:"featurePolicy,omitempty"` IsDevelopment bool `json:"isDevelopment,omitempty" toml:"isDevelopment,omitempty" yaml:"isDevelopment,omitempty"` } @@ -208,6 +209,7 @@ func (h *Headers) HasSecureHeadersDefined() bool { h.ContentSecurityPolicy != "" || h.PublicKey != "" || h.ReferrerPolicy != "" || + h.FeaturePolicy != "" || h.IsDevelopment) } diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml index 2ecf138d2..c0050f7db 100644 --- a/pkg/config/file/fixtures/sample.toml +++ b/pkg/config/file/fixtures/sample.toml @@ -376,6 +376,7 @@ contentSecurityPolicy = "foobar" publicKey = "foobar" referrerPolicy = "foobar" + featurePolicy = "foobar" isDevelopment = true [http.middlewares.Middleware8.headers.customRequestHeaders] name0 = "foobar" @@ -468,4 +469,4 @@ [tls.stores.Store1] [tls.stores.Store1.defaultCertificate] certFile = "foobar" - keyFile = "foobar" \ No newline at end of file + keyFile = "foobar" diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 7c2a031c5..f7a305afd 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -63,6 +63,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware8.headers.isdevelopment": "true", "traefik.http.middlewares.Middleware8.headers.publickey": "foobar", "traefik.http.middlewares.Middleware8.headers.referrerpolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.featurepolicy": "foobar", "traefik.http.middlewares.Middleware8.headers.sslforcehost": "true", "traefik.http.middlewares.Middleware8.headers.sslhost": "foobar", "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name0": "foobar", @@ -487,6 +488,7 @@ func TestDecodeConfiguration(t *testing.T) { ContentSecurityPolicy: "foobar", PublicKey: "foobar", ReferrerPolicy: "foobar", + FeaturePolicy: "foobar", IsDevelopment: true, }, }, @@ -884,6 +886,7 @@ func TestEncodeConfiguration(t *testing.T) { ContentSecurityPolicy: "foobar", PublicKey: "foobar", ReferrerPolicy: "foobar", + FeaturePolicy: "foobar", IsDevelopment: true, }, }, @@ -1020,6 +1023,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.IsDevelopment": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.PublicKey": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.ReferrerPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.FeaturePolicy": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.SSLForceHost": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.SSLHost": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name0": "foobar", diff --git a/pkg/middlewares/headers/headers.go b/pkg/middlewares/headers/headers.go index a4d7de259..d496ba5e5 100644 --- a/pkg/middlewares/headers/headers.go +++ b/pkg/middlewares/headers/headers.go @@ -29,7 +29,6 @@ func New(ctx context.Context, next http.Handler, config dynamic.Headers, name st // HeaderMiddleware -> SecureMiddleWare -> next logger := middlewares.GetLogger(ctx, name, typeName) logger.Debug("Creating middleware") - hasSecureHeaders := config.HasSecureHeadersDefined() hasCustomHeaders := config.HasCustomHeadersDefined() hasCorsHeaders := config.HasCorsHeadersDefined() @@ -94,6 +93,7 @@ func newSecure(next http.Handler, headers dynamic.Headers) *secureHeader { HostsProxyHeaders: headers.HostsProxyHeaders, SSLProxyHeaders: headers.SSLProxyHeaders, STSSeconds: headers.STSSeconds, + FeaturePolicy: headers.FeaturePolicy, } return &secureHeader{ diff --git a/pkg/responsemodifiers/headers.go b/pkg/responsemodifiers/headers.go index aa209ad0a..c7eaa8ee3 100644 --- a/pkg/responsemodifiers/headers.go +++ b/pkg/responsemodifiers/headers.go @@ -30,6 +30,7 @@ func buildHeaders(hdrs *dynamic.Headers) func(*http.Response) error { HostsProxyHeaders: hdrs.HostsProxyHeaders, SSLProxyHeaders: hdrs.SSLProxyHeaders, STSSeconds: hdrs.STSSeconds, + FeaturePolicy: hdrs.FeaturePolicy, } return func(resp *http.Response) error { diff --git a/vendor/github.com/unrolled/secure/secure.go b/vendor/github.com/unrolled/secure/secure.go index 0f8c58017..9e3ca6f5b 100644 --- a/vendor/github.com/unrolled/secure/secure.go +++ b/vendor/github.com/unrolled/secure/secure.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "regexp" "strings" ) @@ -20,8 +21,11 @@ const ( xssProtectionHeader = "X-XSS-Protection" xssProtectionValue = "1; mode=block" cspHeader = "Content-Security-Policy" + cspReportOnlyHeader = "Content-Security-Policy-Report-Only" hpkpHeader = "Public-Key-Pins" referrerPolicyHeader = "Referrer-Policy" + featurePolicyHeader = "Feature-Policy" + expectCTHeader = "Expect-CT" ctxSecureHeaderKey = secureCtxKey("SecureResponseHeader") cspNonceSize = 16 @@ -61,6 +65,8 @@ type Options struct { STSPreload bool // ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "". ContentSecurityPolicy string + // ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "". + ContentSecurityPolicyReportOnly string // CustomBrowserXssValue allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is "". CustomBrowserXssValue string // nolint: golint // Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be later retrieved using the Nonce function. @@ -71,10 +77,15 @@ type Options struct { PublicKey string // ReferrerPolicy allows sites to control when browsers will pass the Referer header to other sites. Default is "". ReferrerPolicy string + // FeaturePolicy allows to selectively enable and disable use of various browser features and APIs. Default is "". + FeaturePolicy string // SSLHost is the host name that is used to redirect http requests to https. Default is "", which indicates to use the same host. SSLHost string // AllowedHosts is a list of fully qualified domain names that are allowed. Default is empty list, which allows any and all host names. AllowedHosts []string + // AllowedHostsAreRegex determines, if the provided slice contains valid regular expressions. If this flag is set to true, every request's + // host will be checked against these expressions. Default is false for backwards compatibility. + AllowedHostsAreRegex bool // HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request. HostsProxyHeaders []string // SSLHostFunc is a function pointer, the return value of the function is the host name that has same functionality as `SSHost`. Default is nil. @@ -84,6 +95,8 @@ type Options struct { SSLProxyHeaders map[string]string // STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header. STSSeconds int64 + // ExpectCTHeader allows the Expect-CT header value to be set with a custom value. Default is "". + ExpectCTHeader string } // Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be @@ -94,6 +107,10 @@ type Secure struct { // badHostHandler is the handler used when an incorrect host is passed in. badHostHandler http.Handler + + // cRegexAllowedHosts saves the compiled regular expressions of the AllowedHosts + // option for subsequent use in processRequest + cRegexAllowedHosts []*regexp.Regexp } // New constructs a new Secure instance with the supplied options. @@ -106,13 +123,27 @@ func New(options ...Options) *Secure { } o.ContentSecurityPolicy = strings.Replace(o.ContentSecurityPolicy, "$NONCE", "'nonce-%[1]s'", -1) + o.ContentSecurityPolicyReportOnly = strings.Replace(o.ContentSecurityPolicyReportOnly, "$NONCE", "'nonce-%[1]s'", -1) - o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s") + o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s") || strings.Contains(o.ContentSecurityPolicyReportOnly, "%[1]s") - return &Secure{ + s := &Secure{ opt: o, badHostHandler: http.HandlerFunc(defaultBadHostHandler), } + + if s.opt.AllowedHostsAreRegex { + // Test for invalid regular expressions in AllowedHosts + for _, allowedHost := range o.AllowedHosts { + regex, err := regexp.Compile(fmt.Sprintf("^%s$", allowedHost)) + if err != nil { + panic(fmt.Sprintf("Error parsing AllowedHost: %s", err)) + } + s.cRegexAllowedHosts = append(s.cRegexAllowedHosts, regex) + } + } + + return s } // SetBadHostHandler sets the handler to call when secure rejects the host name. @@ -123,13 +154,10 @@ func (s *Secure) SetBadHostHandler(handler http.Handler) { // Handler implements the http.HandlerFunc for integration with the standard net/http lib. func (s *Secure) Handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.opt.nonceEnabled { - r = withCSPNonce(r, cspRandNonce()) - } - // Let secure process the request. If it returns an error, // that indicates the request should not continue. - err := s.Process(w, r) + responseHeader, r, err := s.processRequest(w, r) + addResponseHeaders(responseHeader, w) // If there was an error, do not continue. if err != nil { @@ -144,13 +172,9 @@ func (s *Secure) Handler(h http.Handler) http.Handler { // Note that this is for requests only and will not write any headers. func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.opt.nonceEnabled { - r = withCSPNonce(r, cspRandNonce()) - } - // Let secure process the request. If it returns an error, // that indicates the request should not continue. - responseHeader, err := s.processRequest(w, r) + responseHeader, r, err := s.processRequest(w, r) // If there was an error, do not continue. if err != nil { @@ -167,13 +191,10 @@ func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler { // HandlerFuncWithNext is a special implementation for Negroni, but could be used elsewhere. func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if s.opt.nonceEnabled { - r = withCSPNonce(r, cspRandNonce()) - } - // Let secure process the request. If it returns an error, // that indicates the request should not continue. - err := s.Process(w, r) + responseHeader, r, err := s.processRequest(w, r) + addResponseHeaders(responseHeader, w) // If there was an error, do not call next. if err == nil && next != nil { @@ -184,13 +205,9 @@ func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, nex // HandlerFuncWithNextForRequestOnly is a special implementation for Negroni, but could be used elsewhere. // Note that this is for requests only and will not write any headers. func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if s.opt.nonceEnabled { - r = withCSPNonce(r, cspRandNonce()) - } - // Let secure process the request. If it returns an error, // that indicates the request should not continue. - responseHeader, err := s.processRequest(w, r) + responseHeader, r, err := s.processRequest(w, r) // If there was an error, do not call next. if err == nil && next != nil { @@ -202,21 +219,37 @@ func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *htt } } -// Process runs the actual checks and writes the headers in the ResponseWriter. -func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error { - responseHeader, err := s.processRequest(w, r) +// addResponseHeaders Adds the headers from 'responseHeader' to the response. +func addResponseHeaders(responseHeader http.Header, w http.ResponseWriter) { if responseHeader != nil { for key, values := range responseHeader { for _, value := range values { - w.Header().Add(key, value) + w.Header().Set(key, value) } } } +} + +// Process runs the actual checks and writes the headers in the ResponseWriter. +func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error { + responseHeader, _, err := s.processRequest(w, r) + addResponseHeaders(responseHeader, w) + return err } +// ProcessNoModifyRequest runs the actual checks but does not write the headers in the ResponseWriter. +func (s *Secure) ProcessNoModifyRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) { + return s.processRequest(w, r) +} + // processRequest runs the actual checks on the request and returns an error if the middleware chain should stop. -func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, error) { +func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) { + // Setup nonce if required. + if s.opt.nonceEnabled { + r = withCSPNonce(r, cspRandNonce()) + } + // Resolve the host for the request, using proxy headers if present. host := r.Host for _, header := range s.opt.HostsProxyHeaders { @@ -229,16 +262,25 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He // Allowed hosts check. if len(s.opt.AllowedHosts) > 0 && !s.opt.IsDevelopment { isGoodHost := false - for _, allowedHost := range s.opt.AllowedHosts { - if strings.EqualFold(allowedHost, host) { - isGoodHost = true - break + if s.opt.AllowedHostsAreRegex { + for _, allowedHost := range s.cRegexAllowedHosts { + if match := allowedHost.MatchString(host); match { + isGoodHost = true + break + } + } + } else { + for _, allowedHost := range s.opt.AllowedHosts { + if strings.EqualFold(allowedHost, host) { + isGoodHost = true + break + } } } if !isGoodHost { s.badHostHandler.ServeHTTP(w, r) - return nil, fmt.Errorf("bad host name: %s", host) + return nil, nil, fmt.Errorf("bad host name: %s", host) } } @@ -265,11 +307,11 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He } http.Redirect(w, r, url.String(), status) - return nil, fmt.Errorf("redirecting to HTTPS") + return nil, nil, fmt.Errorf("redirecting to HTTPS") } if s.opt.SSLForceHost { - var SSLHost = host; + var SSLHost = host if s.opt.SSLHostFunc != nil { if h := (*s.opt.SSLHostFunc)(host); len(h) > 0 { SSLHost = h @@ -288,7 +330,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He } http.Redirect(w, r, url.String(), status) - return nil, fmt.Errorf("redirecting to HTTPS") + return nil, nil, fmt.Errorf("redirecting to HTTPS") } } @@ -343,12 +385,31 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He } } + // Content Security Policy Report Only header. + if len(s.opt.ContentSecurityPolicyReportOnly) > 0 { + if s.opt.nonceEnabled { + responseHeader.Set(cspReportOnlyHeader, fmt.Sprintf(s.opt.ContentSecurityPolicyReportOnly, CSPNonce(r.Context()))) + } else { + responseHeader.Set(cspReportOnlyHeader, s.opt.ContentSecurityPolicyReportOnly) + } + } + // Referrer Policy header. if len(s.opt.ReferrerPolicy) > 0 { responseHeader.Set(referrerPolicyHeader, s.opt.ReferrerPolicy) } - return responseHeader, nil + // Feature Policy header. + if len(s.opt.FeaturePolicy) > 0 { + responseHeader.Set(featurePolicyHeader, s.opt.FeaturePolicy) + } + + // Expect-CT header. + if len(s.opt.ExpectCTHeader) > 0 { + responseHeader.Set(expectCTHeader, s.opt.ExpectCTHeader) + } + + return responseHeader, r, nil } // isSSL determine if we are on HTTPS.