config: add configurable content security policy (#18085)
This commit is contained in:
parent
c25c04816d
commit
9f19d7c373
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: Added configurable content security policy header
|
||||||
|
```
|
|
@ -37,6 +37,7 @@ import (
|
||||||
"github.com/hashicorp/nomad/helper/tlsutil"
|
"github.com/hashicorp/nomad/helper/tlsutil"
|
||||||
"github.com/hashicorp/nomad/nomad"
|
"github.com/hashicorp/nomad/nomad"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -505,7 +506,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
||||||
uiConfigEnabled := agentConfig.UI != nil && agentConfig.UI.Enabled
|
uiConfigEnabled := agentConfig.UI != nil && agentConfig.UI.Enabled
|
||||||
|
|
||||||
if uiEnabled && uiConfigEnabled {
|
if uiEnabled && uiConfigEnabled {
|
||||||
s.mux.Handle("/ui/", http.StripPrefix("/ui/", s.handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()}))))
|
s.mux.Handle("/ui/", http.StripPrefix("/ui/", s.handleUI(agentConfig.UI.ContentSecurityPolicy, http.FileServer(&UIAssetWrapper{FileSystem: assetFS()}))))
|
||||||
s.logger.Debug("UI is enabled")
|
s.logger.Debug("UI is enabled")
|
||||||
} else {
|
} else {
|
||||||
// Write the stubHTML
|
// Write the stubHTML
|
||||||
|
@ -646,10 +647,10 @@ func (e *codedError) Code() int {
|
||||||
return e.code
|
return e.code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HTTPServer) handleUI(h http.Handler) http.Handler {
|
func (s *HTTPServer) handleUI(policy *config.ContentSecurityPolicy, h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
header := w.Header()
|
header := w.Header()
|
||||||
header.Add("Content-Security-Policy", "default-src 'none'; connect-src *; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'none'; frame-ancestors 'none'")
|
header.Add("Content-Security-Policy", policy.String())
|
||||||
h.ServeHTTP(w, req)
|
h.ServeHTTP(w, req)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,13 @@
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
// UIConfig contains the operator configuration of the web UI
|
// UIConfig contains the operator configuration of the web UI
|
||||||
// Note:
|
// Note:
|
||||||
// before extending this configuration, consider reviewing NMD-125
|
// before extending this configuration, consider reviewing NMD-125
|
||||||
|
@ -11,6 +18,9 @@ type UIConfig struct {
|
||||||
// Enabled is used to enable the web UI
|
// Enabled is used to enable the web UI
|
||||||
Enabled bool `hcl:"enabled"`
|
Enabled bool `hcl:"enabled"`
|
||||||
|
|
||||||
|
// ContentSecurityPolicy is used to configure the CSP header
|
||||||
|
ContentSecurityPolicy *ContentSecurityPolicy `hcl:"content_security_policy"`
|
||||||
|
|
||||||
// Consul configures deep links for Consul UI
|
// Consul configures deep links for Consul UI
|
||||||
Consul *ConsulUIConfig `hcl:"consul"`
|
Consul *ConsulUIConfig `hcl:"consul"`
|
||||||
|
|
||||||
|
@ -21,6 +31,87 @@ type UIConfig struct {
|
||||||
Label *LabelUIConfig `hcl:"label"`
|
Label *LabelUIConfig `hcl:"label"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only covers the elements of
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP we need or care about
|
||||||
|
type ContentSecurityPolicy struct {
|
||||||
|
ConnectSrc []string `hcl:"connect_src"`
|
||||||
|
DefaultSrc []string `hcl:"default_src"`
|
||||||
|
FormAction []string `hcl:"form_action"`
|
||||||
|
FrameAncestors []string `hcl:"frame_ancestors"`
|
||||||
|
ImgSrc []string `hcl:"img_src"`
|
||||||
|
ScriptSrc []string `hcl:"script_src"`
|
||||||
|
StyleSrc []string `hcl:"style_src"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of this Vault UI config.
|
||||||
|
func (old *ContentSecurityPolicy) Copy() *ContentSecurityPolicy {
|
||||||
|
if old == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nc := new(ContentSecurityPolicy)
|
||||||
|
*nc = *old
|
||||||
|
nc.ConnectSrc = slices.Clone(old.ConnectSrc)
|
||||||
|
nc.DefaultSrc = slices.Clone(old.DefaultSrc)
|
||||||
|
nc.FormAction = slices.Clone(old.FormAction)
|
||||||
|
nc.FrameAncestors = slices.Clone(old.FrameAncestors)
|
||||||
|
nc.ImgSrc = slices.Clone(old.ImgSrc)
|
||||||
|
nc.ScriptSrc = slices.Clone(old.ScriptSrc)
|
||||||
|
nc.StyleSrc = slices.Clone(old.StyleSrc)
|
||||||
|
return nc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csp *ContentSecurityPolicy) String() string {
|
||||||
|
return fmt.Sprintf("default-src %s; connect-src %s; img-src %s; script-src %s; style-src %s; form-action %s; frame-ancestors %s", strings.Join(csp.DefaultSrc, " "), strings.Join(csp.ConnectSrc, " "), strings.Join(csp.ImgSrc, " "), strings.Join(csp.ScriptSrc, " "), strings.Join(csp.StyleSrc, " "), strings.Join(csp.FormAction, " "), strings.Join(csp.FrameAncestors, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (csp *ContentSecurityPolicy) Merge(other *ContentSecurityPolicy) *ContentSecurityPolicy {
|
||||||
|
result := csp.Copy()
|
||||||
|
if result == nil {
|
||||||
|
result = &ContentSecurityPolicy{}
|
||||||
|
}
|
||||||
|
if other == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(other.ConnectSrc) > 0 {
|
||||||
|
result.ConnectSrc = other.ConnectSrc
|
||||||
|
}
|
||||||
|
if len(other.DefaultSrc) > 0 {
|
||||||
|
result.DefaultSrc = other.DefaultSrc
|
||||||
|
}
|
||||||
|
if len(other.FormAction) > 0 {
|
||||||
|
result.FormAction = other.FormAction
|
||||||
|
}
|
||||||
|
if len(other.FrameAncestors) > 0 {
|
||||||
|
result.FrameAncestors = other.FrameAncestors
|
||||||
|
}
|
||||||
|
if len(other.ImgSrc) > 0 {
|
||||||
|
result.ImgSrc = other.ImgSrc
|
||||||
|
}
|
||||||
|
if len(other.ScriptSrc) > 0 {
|
||||||
|
result.ScriptSrc = other.ScriptSrc
|
||||||
|
}
|
||||||
|
if len(other.StyleSrc) > 0 {
|
||||||
|
result.StyleSrc = other.StyleSrc
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultCSPConfig() *ContentSecurityPolicy {
|
||||||
|
return &ContentSecurityPolicy{
|
||||||
|
ConnectSrc: []string{"*"},
|
||||||
|
DefaultSrc: []string{"'none'"},
|
||||||
|
FormAction: []string{"'none'"},
|
||||||
|
FrameAncestors: []string{"'none'"},
|
||||||
|
ImgSrc: []string{"'self'", "data:"},
|
||||||
|
ScriptSrc: []string{"'self'"},
|
||||||
|
StyleSrc: []string{"'self'", "'unsafe-inline'"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ConsulUIConfig configures deep links to this cluster's Consul
|
// ConsulUIConfig configures deep links to this cluster's Consul
|
||||||
type ConsulUIConfig struct {
|
type ConsulUIConfig struct {
|
||||||
|
|
||||||
|
@ -47,10 +138,11 @@ type LabelUIConfig struct {
|
||||||
// `ui` configuration.
|
// `ui` configuration.
|
||||||
func DefaultUIConfig() *UIConfig {
|
func DefaultUIConfig() *UIConfig {
|
||||||
return &UIConfig{
|
return &UIConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Consul: &ConsulUIConfig{},
|
Consul: &ConsulUIConfig{},
|
||||||
Vault: &VaultUIConfig{},
|
Vault: &VaultUIConfig{},
|
||||||
Label: &LabelUIConfig{},
|
Label: &LabelUIConfig{},
|
||||||
|
ContentSecurityPolicy: DefaultCSPConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +176,7 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig {
|
||||||
result.Consul = result.Consul.Merge(other.Consul)
|
result.Consul = result.Consul.Merge(other.Consul)
|
||||||
result.Vault = result.Vault.Merge(other.Vault)
|
result.Vault = result.Vault.Merge(other.Vault)
|
||||||
result.Label = result.Label.Merge(other.Label)
|
result.Label = result.Label.Merge(other.Label)
|
||||||
|
result.ContentSecurityPolicy = result.ContentSecurityPolicy.Merge(other.ContentSecurityPolicy)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ func TestUIConfig_Merge(t *testing.T) {
|
||||||
BackgroundColor: "blue",
|
BackgroundColor: "blue",
|
||||||
TextColor: "#fff",
|
TextColor: "#fff",
|
||||||
},
|
},
|
||||||
|
ContentSecurityPolicy: DefaultCSPConfig(),
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
@ -64,6 +65,7 @@ func TestUIConfig_Merge(t *testing.T) {
|
||||||
Consul: &ConsulUIConfig{
|
Consul: &ConsulUIConfig{
|
||||||
BaseUIURL: "http://consul-other.example.com:8500",
|
BaseUIURL: "http://consul-other.example.com:8500",
|
||||||
},
|
},
|
||||||
|
ContentSecurityPolicy: DefaultCSPConfig(),
|
||||||
},
|
},
|
||||||
right: &UIConfig{},
|
right: &UIConfig{},
|
||||||
expect: &UIConfig{
|
expect: &UIConfig{
|
||||||
|
@ -71,8 +73,9 @@ func TestUIConfig_Merge(t *testing.T) {
|
||||||
Consul: &ConsulUIConfig{
|
Consul: &ConsulUIConfig{
|
||||||
BaseUIURL: "http://consul-other.example.com:8500",
|
BaseUIURL: "http://consul-other.example.com:8500",
|
||||||
},
|
},
|
||||||
Vault: &VaultUIConfig{},
|
Vault: &VaultUIConfig{},
|
||||||
Label: &LabelUIConfig{},
|
Label: &LabelUIConfig{},
|
||||||
|
ContentSecurityPolicy: DefaultCSPConfig(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,16 @@ The `ui` block configures the Nomad agent's [web UI].
|
||||||
ui {
|
ui {
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
content_security_policy {
|
||||||
|
connect_src = ["*"]
|
||||||
|
default_src = ["'none'"]
|
||||||
|
form_action = ["'none'"]
|
||||||
|
frame_ancestors = ["'none'"]
|
||||||
|
img_src = ["'self'","data:"]
|
||||||
|
script_src = ["'self'"]
|
||||||
|
style_src = ["'self'","'unsafe-inline'"]
|
||||||
|
}
|
||||||
|
|
||||||
consul {
|
consul {
|
||||||
ui_url = "https://consul.example.com:8501/ui"
|
ui_url = "https://consul.example.com:8501/ui"
|
||||||
}
|
}
|
||||||
|
@ -48,6 +58,27 @@ and the configuration is individual to each agent.
|
||||||
- `label` <code>([Label]: nil)</code> - Configures a user-defined
|
- `label` <code>([Label]: nil)</code> - Configures a user-defined
|
||||||
label to display in the Nomad Web UI header.
|
label to display in the Nomad Web UI header.
|
||||||
|
|
||||||
|
## `content_security_policy` Parameters
|
||||||
|
|
||||||
|
The `content_security_policy` block configures the HTTP
|
||||||
|
[Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
||||||
|
header sent with the web UI response.
|
||||||
|
|
||||||
|
- `connect_src` `(array<string>: ["*"])` - Specifies the valid sources for
|
||||||
|
`connect-src` in the Content Security Policy header.
|
||||||
|
- `default_src` `(array<string>: ["'none'"])` - Specifies the valid sources for
|
||||||
|
`default-src` in the Content Security Policy header.
|
||||||
|
- `form_action` `(array<string>: ["'none'"])` - Specifies the valid sources for
|
||||||
|
`form-action` in the Content Security Policy header.
|
||||||
|
- `frame_ancestors` `(array<string>: ["'none'"])` - Specifies the valid sources
|
||||||
|
for `frame-ancestors` in the Content Security Policy header.
|
||||||
|
- `img_src` `(array<string>: ["'self'", "data:"])` - Specifies the valid sources
|
||||||
|
for `img-src` in the Content Security Policy header.
|
||||||
|
- `script_src` `(array<string>: ["'self'"])` - Specifies the valid sources for
|
||||||
|
`script-src` in the Content Security Policy header.
|
||||||
|
- `style_src` `(array<string>: ["'self'","'unsafe-inline'"])` - Specifies the
|
||||||
|
valid sources for `style-src` in the Content Security Policy header.
|
||||||
|
|
||||||
## `consul` Parameters
|
## `consul` Parameters
|
||||||
|
|
||||||
- `ui_url` `(string: "")` - Specifies the full base URL to a Consul
|
- `ui_url` `(string: "")` - Specifies the full base URL to a Consul
|
||||||
|
|
Loading…
Reference in New Issue