diff --git a/cmd/registry/config-cache.yml b/cmd/registry/config-cache.yml index 0b524043..6566130b 100644 --- a/cmd/registry/config-cache.yml +++ b/cmd/registry/config-cache.yml @@ -17,6 +17,8 @@ http: secret: asecretforlocaldevelopment debug: addr: localhost:5001 + headers: + X-Content-Type-Options: [nosniff] redis: addr: localhost:6379 pool: diff --git a/cmd/registry/config-dev.yml b/cmd/registry/config-dev.yml index 3f4616d8..729e7fd2 100644 --- a/cmd/registry/config-dev.yml +++ b/cmd/registry/config-dev.yml @@ -32,6 +32,8 @@ http: addr: :5000 debug: addr: localhost:5001 + headers: + X-Content-Type-Options: [nosniff] redis: addr: localhost:6379 pool: diff --git a/cmd/registry/config-example.yml b/cmd/registry/config-example.yml index cb91e63d..8cfe06fa 100644 --- a/cmd/registry/config-example.yml +++ b/cmd/registry/config-example.yml @@ -9,3 +9,5 @@ storage: rootdirectory: /var/lib/registry http: addr: :5000 + headers: + X-Content-Type-Options: [nosniff] diff --git a/configuration/configuration.go b/configuration/configuration.go index 502dab3e..c6554f45 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -86,6 +86,12 @@ type Configuration struct { ClientCAs []string `yaml:"clientcas,omitempty"` } `yaml:"tls,omitempty"` + // Headers is a set of headers to include in HTTP responses. A common + // use case for this would be security headers such as + // Strict-Transport-Security. The map keys are the header names, and + // the values are the associated header payloads. + Headers http.Header `yaml:"headers,omitempty"` + // Debug configures the http debug interface, if specified. This can // include services such as pprof, expvar and other data that should // not be exposed externally. Left disabled by default. diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 24076e2c..4790f21f 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -70,7 +70,8 @@ var configStruct = Configuration{ Key string `yaml:"key,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"` } `yaml:"tls,omitempty"` - Debug struct { + Headers http.Header `yaml:"headers,omitempty"` + Debug struct { Addr string `yaml:"addr,omitempty"` } `yaml:"debug,omitempty"` }{ @@ -81,6 +82,9 @@ var configStruct = Configuration{ }{ ClientCAs: []string{"/path/to/ca.pem"}, }, + Headers: http.Header{ + "X-Content-Type-Options": []string{"nosniff"}, + }, }, } @@ -118,6 +122,8 @@ reporting: http: clientcas: - /path/to/ca.pem + headers: + X-Content-Type-Options: [nosniff] ` // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory @@ -136,6 +142,9 @@ notifications: url: http://example.com headers: Authorization: [Bearer ] +http: + headers: + X-Content-Type-Options: [nosniff] ` type ConfigSuite struct { @@ -192,6 +201,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *C) { suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}} suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Notifications = Notifications{} + suite.expectedConfig.HTTP.Headers = nil os.Setenv("REGISTRY_STORAGE", "filesystem") os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot") @@ -366,5 +376,10 @@ func copyConfig(config Configuration) *Configuration { configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v) } + configCopy.HTTP.Headers = make(http.Header) + for k, v := range config.HTTP.Headers { + configCopy.HTTP.Headers[k] = v + } + return configCopy } diff --git a/docs/configuration.md b/docs/configuration.md index f2f58a4d..28c9d6f7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -163,6 +163,8 @@ information about each option that appears later in this page. - /path/to/another/ca.pem debug: addr: localhost:5001 + headers: + X-Content-Type-Options: [nosniff] notifications: endpoints: - name: alistener @@ -1147,6 +1149,8 @@ configuration may contain both. - /path/to/another/ca.pem debug: addr: localhost:5001 + headers: + X-Content-Type-Options: [nosniff] The `http` option details the configuration for the HTTP server that hosts the registry. @@ -1275,6 +1279,21 @@ The `debug` section takes a single, required `addr` parameter. This parameter specifies the `HOST:PORT` on which the debug server should accept connections. +### headers + +The `headers` option is **optional** . Use it to specify headers that the HTTP +server should include in responses. This can be used for security headers such +as `Strict-Transport-Security`. + +The `headers` option should contain an option for each header to include, where +the parameter name is the header's name, and the parameter value a list of the +header's payload values. + +Including `X-Content-Type-Options: [nosniff]` is recommended, so that browsers +will not interpret content as HTML if they are directed to load a page from the +registry. This header is included in the example configuration files. + + ## notifications notifications: diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go index c484835f..0e192449 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -30,6 +30,10 @@ import ( "golang.org/x/net/context" ) +var headerConfig = http.Header{ + "X-Content-Type-Options": []string{"nosniff"}, +} + // TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified // 200 OK response. func TestCheckAPI(t *testing.T) { @@ -215,6 +219,7 @@ func TestURLPrefix(t *testing.T) { }, } config.HTTP.Prefix = "/test/" + config.HTTP.Headers = headerConfig env := newTestEnvWithConfig(t, &config) @@ -1009,6 +1014,8 @@ func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv { }, } + config.HTTP.Headers = headerConfig + return newTestEnvWithConfig(t, &config) } @@ -1225,6 +1232,14 @@ func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus t.FailNow() } + + // We expect the headers included in the configuration + if !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) { + t.Logf("missing or incorrect header X-Content-Type-Options %s", msg) + maybeDumpResponse(t, resp) + + t.FailNow() + } } // checkBodyHasErrorCodes ensures the body is an error body and has the diff --git a/registry/handlers/app.go b/registry/handlers/app.go index f60290d0..7b0fe6c2 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -428,6 +428,12 @@ type dispatchFunc func(ctx *Context, r *http.Request) http.Handler // handler, using the dispatch factory function. func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for headerName, headerValues := range app.Config.HTTP.Headers { + for _, value := range headerValues { + w.Header().Add(headerName, value) + } + } + context := app.context(w, r) if err := app.authorized(w, r, context); err != nil {