Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added config flag to disable frame-ancestor for the nomad UI #18085

Merged
merged 12 commits into from
Aug 14, 2023
Merged
3 changes: 3 additions & 0 deletions .changelog/18085.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Added configurable content security policy header
```
7 changes: 4 additions & 3 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
)

const (
Expand Down Expand Up @@ -505,7 +506,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
uiConfigEnabled := agentConfig.UI != nil && agentConfig.UI.Enabled

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")
} else {
// Write the stubHTML
Expand Down Expand Up @@ -646,10 +647,10 @@ func (e *codedError) Code() int {
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) {
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)
})
}
Expand Down
92 changes: 88 additions & 4 deletions nomad/structs/config/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

package config

import (
"fmt"
"strings"
)

// UIConfig contains the operator configuration of the web UI
// Note:
// before extending this configuration, consider reviewing NMD-125
Expand All @@ -11,6 +16,9 @@ type UIConfig struct {
// Enabled is used to enable the web UI
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 *ConsulUIConfig `hcl:"consul"`

Expand All @@ -21,6 +29,80 @@ type UIConfig struct {
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
ebarriosjr marked this conversation as resolved.
Show resolved Hide resolved
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{}
ebarriosjr marked this conversation as resolved.
Show resolved Hide resolved
}
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
type ConsulUIConfig struct {

Expand All @@ -47,10 +129,11 @@ type LabelUIConfig struct {
// `ui` configuration.
func DefaultUIConfig() *UIConfig {
return &UIConfig{
Enabled: true,
Consul: &ConsulUIConfig{},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
Enabled: true,
Consul: &ConsulUIConfig{},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
ContentSecurityPolicy: DefaultCSPConfig(),
}
}

Expand Down Expand Up @@ -84,6 +167,7 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig {
result.Consul = result.Consul.Merge(other.Consul)
result.Vault = result.Vault.Merge(other.Vault)
result.Label = result.Label.Merge(other.Label)
result.ContentSecurityPolicy = result.ContentSecurityPolicy.Merge(other.ContentSecurityPolicy)

return result
}
Expand Down
7 changes: 5 additions & 2 deletions nomad/structs/config/ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestUIConfig_Merge(t *testing.T) {
BackgroundColor: "blue",
TextColor: "#fff",
},
ContentSecurityPolicy: DefaultCSPConfig(),
}

testCases := []struct {
Expand Down Expand Up @@ -64,15 +65,17 @@ func TestUIConfig_Merge(t *testing.T) {
Consul: &ConsulUIConfig{
BaseUIURL: "http://consul-other.example.com:8500",
},
ContentSecurityPolicy: DefaultCSPConfig(),
},
right: &UIConfig{},
expect: &UIConfig{
Enabled: false,
Consul: &ConsulUIConfig{
BaseUIURL: "http://consul-other.example.com:8500",
},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
ContentSecurityPolicy: DefaultCSPConfig(),
},
},
}
Expand Down
34 changes: 34 additions & 0 deletions website/content/docs/configuration/ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ The `ui` block configures the Nomad agent's [web UI].
ui {
enabled = true

content_security_policy {
connect_src = ["*"]
default_src = ["none"]
form_action = ["none"]
frame_ancestors = ["none"]
ebarriosjr marked this conversation as resolved.
Show resolved Hide resolved
img_src = ["'self'","data:"]
script_src = ["'self'"]
style_src = ["'self'","'unsafe-inline'"]
}

consul {
ui_url = "https://consul.example.com:8501/ui"
}
Expand Down Expand Up @@ -48,6 +58,30 @@ and the configuration is individual to each agent.
- `label` <code>([Label]: nil)</code> - Configures a user-defined
label to display in the Nomad Web UI header.

## `content_security_policy` Parameters

- `connect_src` `(array<string>: [])` - Specifies the valid sources for
`connect-src` in the Content Security Policy header. The default
value is `["*"]`.
- `default_src` `(array<string>: [])` - Specifies the valid sources for
`default-src` in the Content Security Policy header. The default
value is `["none"]`.
- `form_action` `(array<string>: [])` - Specifies the valid sources for
`form-action` in the Content Security Policy header. The default
value is `["none"]`.
- `frame_ancestors` `(array<string>: [])` - Specifies the valid sources
for `frame-ancestors` in the Content Security Policy header. The
default value is `["none"]`.
- `img_src` `(array<string>: [])` - Specifies the valid sources
for `img-src` in the Content Security Policy header. The default
value is `["'self'","data:"]`.
- `script_src` `(array<string>: [])` - Specifies the valid sources for
`script-src` in the Content Security Policy header. The default
value is `["'self'"]`.
- `style_src` `(array<string>: [])` - Specifies the
valid sources for `style-src` in the Content Security Policy
header. The default value is `["'self'","'unsafe-inline'"]`.

ebarriosjr marked this conversation as resolved.
Show resolved Hide resolved
## `consul` Parameters

- `ui_url` `(string: "")` - Specifies the full base URL to a Consul
Expand Down