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>
This commit is contained in:
parent
4f7cb60190
commit
9c3bed6b88
8 changed files with 68 additions and 1 deletions
|
@ -17,6 +17,8 @@ http:
|
||||||
secret: asecretforlocaldevelopment
|
secret: asecretforlocaldevelopment
|
||||||
debug:
|
debug:
|
||||||
addr: localhost:5001
|
addr: localhost:5001
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
redis:
|
redis:
|
||||||
addr: localhost:6379
|
addr: localhost:6379
|
||||||
pool:
|
pool:
|
||||||
|
|
|
@ -32,6 +32,8 @@ http:
|
||||||
addr: :5000
|
addr: :5000
|
||||||
debug:
|
debug:
|
||||||
addr: localhost:5001
|
addr: localhost:5001
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
redis:
|
redis:
|
||||||
addr: localhost:6379
|
addr: localhost:6379
|
||||||
pool:
|
pool:
|
||||||
|
|
|
@ -9,3 +9,5 @@ storage:
|
||||||
rootdirectory: /var/lib/registry
|
rootdirectory: /var/lib/registry
|
||||||
http:
|
http:
|
||||||
addr: :5000
|
addr: :5000
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
|
|
|
@ -86,6 +86,12 @@ type Configuration struct {
|
||||||
ClientCAs []string `yaml:"clientcas,omitempty"`
|
ClientCAs []string `yaml:"clientcas,omitempty"`
|
||||||
} `yaml:"tls,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
|
// Debug configures the http debug interface, if specified. This can
|
||||||
// include services such as pprof, expvar and other data that should
|
// include services such as pprof, expvar and other data that should
|
||||||
// not be exposed externally. Left disabled by default.
|
// not be exposed externally. Left disabled by default.
|
||||||
|
|
|
@ -70,6 +70,7 @@ var configStruct = Configuration{
|
||||||
Key string `yaml:"key,omitempty"`
|
Key string `yaml:"key,omitempty"`
|
||||||
ClientCAs []string `yaml:"clientcas,omitempty"`
|
ClientCAs []string `yaml:"clientcas,omitempty"`
|
||||||
} `yaml:"tls,omitempty"`
|
} `yaml:"tls,omitempty"`
|
||||||
|
Headers http.Header `yaml:"headers,omitempty"`
|
||||||
Debug struct {
|
Debug struct {
|
||||||
Addr string `yaml:"addr,omitempty"`
|
Addr string `yaml:"addr,omitempty"`
|
||||||
} `yaml:"debug,omitempty"`
|
} `yaml:"debug,omitempty"`
|
||||||
|
@ -81,6 +82,9 @@ var configStruct = Configuration{
|
||||||
}{
|
}{
|
||||||
ClientCAs: []string{"/path/to/ca.pem"},
|
ClientCAs: []string{"/path/to/ca.pem"},
|
||||||
},
|
},
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Content-Type-Options": []string{"nosniff"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +122,8 @@ reporting:
|
||||||
http:
|
http:
|
||||||
clientcas:
|
clientcas:
|
||||||
- /path/to/ca.pem
|
- /path/to/ca.pem
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
`
|
`
|
||||||
|
|
||||||
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
|
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
|
||||||
|
@ -136,6 +142,9 @@ notifications:
|
||||||
url: http://example.com
|
url: http://example.com
|
||||||
headers:
|
headers:
|
||||||
Authorization: [Bearer <example>]
|
Authorization: [Bearer <example>]
|
||||||
|
http:
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
`
|
`
|
||||||
|
|
||||||
type ConfigSuite struct {
|
type ConfigSuite struct {
|
||||||
|
@ -192,6 +201,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *C) {
|
||||||
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
|
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
|
||||||
suite.expectedConfig.Reporting = Reporting{}
|
suite.expectedConfig.Reporting = Reporting{}
|
||||||
suite.expectedConfig.Notifications = Notifications{}
|
suite.expectedConfig.Notifications = Notifications{}
|
||||||
|
suite.expectedConfig.HTTP.Headers = nil
|
||||||
|
|
||||||
os.Setenv("REGISTRY_STORAGE", "filesystem")
|
os.Setenv("REGISTRY_STORAGE", "filesystem")
|
||||||
os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
|
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.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
|
return configCopy
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,8 @@ information about each option that appears later in this page.
|
||||||
- /path/to/another/ca.pem
|
- /path/to/another/ca.pem
|
||||||
debug:
|
debug:
|
||||||
addr: localhost:5001
|
addr: localhost:5001
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
notifications:
|
notifications:
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: alistener
|
- name: alistener
|
||||||
|
@ -1147,6 +1149,8 @@ configuration may contain both.
|
||||||
- /path/to/another/ca.pem
|
- /path/to/another/ca.pem
|
||||||
debug:
|
debug:
|
||||||
addr: localhost:5001
|
addr: localhost:5001
|
||||||
|
headers:
|
||||||
|
X-Content-Type-Options: [nosniff]
|
||||||
|
|
||||||
The `http` option details the configuration for the HTTP server that hosts the registry.
|
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.
|
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
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
|
|
|
@ -30,6 +30,10 @@ import (
|
||||||
"golang.org/x/net/context"
|
"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
|
// TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified
|
||||||
// 200 OK response.
|
// 200 OK response.
|
||||||
func TestCheckAPI(t *testing.T) {
|
func TestCheckAPI(t *testing.T) {
|
||||||
|
@ -215,6 +219,7 @@ func TestURLPrefix(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.HTTP.Prefix = "/test/"
|
config.HTTP.Prefix = "/test/"
|
||||||
|
config.HTTP.Headers = headerConfig
|
||||||
|
|
||||||
env := newTestEnvWithConfig(t, &config)
|
env := newTestEnvWithConfig(t, &config)
|
||||||
|
|
||||||
|
@ -1009,6 +1014,8 @@ func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.HTTP.Headers = headerConfig
|
||||||
|
|
||||||
return newTestEnvWithConfig(t, &config)
|
return newTestEnvWithConfig(t, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1225,6 +1232,14 @@ func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus
|
||||||
|
|
||||||
t.FailNow()
|
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
|
// 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.
|
// handler, using the dispatch factory function.
|
||||||
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
context := app.context(w, r)
|
||||||
|
|
||||||
if err := app.authorized(w, r, context); err != nil {
|
if err := app.authorized(w, r, context); err != nil {
|
||||||
|
|
Loading…
Reference in a new issue