Add a section to the config file for HTTP headers to add to responses
The example configuration files add X-Content-Type-Options: nosniff. Add coverage in existing registry/handlers unit tests. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>master
							parent
							
								
									4f7cb60190
								
							
						
					
					
						commit
						9c3bed6b88
					
				|  | @ -17,6 +17,8 @@ http: | |||
|     secret: asecretforlocaldevelopment | ||||
|     debug: | ||||
|         addr: localhost:5001 | ||||
|     headers: | ||||
|         X-Content-Type-Options: [nosniff] | ||||
| redis: | ||||
|   addr: localhost:6379 | ||||
|   pool: | ||||
|  |  | |||
|  | @ -32,6 +32,8 @@ http: | |||
|     addr: :5000 | ||||
|     debug: | ||||
|         addr: localhost:5001 | ||||
|     headers: | ||||
|         X-Content-Type-Options: [nosniff] | ||||
| redis: | ||||
|   addr: localhost:6379 | ||||
|   pool: | ||||
|  |  | |||
|  | @ -9,3 +9,5 @@ storage: | |||
|         rootdirectory: /var/lib/registry | ||||
| http: | ||||
|     addr: :5000 | ||||
|     headers: | ||||
|         X-Content-Type-Options: [nosniff] | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -70,6 +70,7 @@ var configStruct = Configuration{ | |||
| 			Key         string   `yaml:"key,omitempty"` | ||||
| 			ClientCAs   []string `yaml:"clientcas,omitempty"` | ||||
| 		} `yaml:"tls,omitempty"` | ||||
| 		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 <example>] | ||||
| 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 | ||||
| } | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue