mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
chore: add deployment config option to append custom csp directives (#15596)
Allows adding custom static CSP directives to Coder. Niche use case but makes this easier then creating a reverse proxy that has to replace the header. We want to preserve our directives, so having an append option is preferred to a "replace" option via a reverse proxy. Closes https://github.com/coder/coder/issues/15118
This commit is contained in:
+7
@@ -294,6 +294,13 @@ backed by Tailscale and WireGuard.
|
||||
+ 1`. Use special value 'disable' to turn off STUN completely.
|
||||
|
||||
NETWORKING / HTTP OPTIONS:
|
||||
--additional-csp-policy string-array, $CODER_ADDITIONAL_CSP_POLICY
|
||||
Coder configures a Content Security Policy (CSP) to protect against
|
||||
XSS attacks. This setting allows you to add additional CSP directives,
|
||||
which can open the attack surface of the deployment. Format matches
|
||||
the CSP directive format, e.g. --additional-csp-policy="script-src
|
||||
https://example.com".
|
||||
|
||||
--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
|
||||
Disable password authentication. This is recommended for security
|
||||
purposes in production deployments that rely on an identity provider.
|
||||
|
||||
+6
@@ -16,6 +16,12 @@ networking:
|
||||
# HTTP bind address of the server. Unset to disable the HTTP endpoint.
|
||||
# (default: 127.0.0.1:3000, type: string)
|
||||
httpAddress: 127.0.0.1:3000
|
||||
# Coder configures a Content Security Policy (CSP) to protect against XSS attacks.
|
||||
# This setting allows you to add additional CSP directives, which can open the
|
||||
# attack surface of the deployment. Format matches the CSP directive format, e.g.
|
||||
# --additional-csp-policy="script-src https://example.com".
|
||||
# (default: <unset>, type: string-array)
|
||||
additionalCSPPolicy: []
|
||||
# The maximum lifetime duration users can specify when creating an API token.
|
||||
# (default: 876600h0m0s, type: duration)
|
||||
maxTokenLifetime: 876600h0m0s
|
||||
|
||||
Generated
+6
@@ -10537,6 +10537,12 @@ const docTemplate = `{
|
||||
"access_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
"additional_csp_policy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
|
||||
"allOf": [
|
||||
|
||||
Generated
+6
@@ -9403,6 +9403,12 @@
|
||||
"access_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
"additional_csp_policy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"description": "DEPRECATED: Use HTTPAddress or TLS.Address instead.",
|
||||
"allOf": [
|
||||
|
||||
+22
-1
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -1378,6 +1379,26 @@ func New(options *Options) *API {
|
||||
r.Get("/swagger/*", swaggerDisabled)
|
||||
}
|
||||
|
||||
additionalCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(api.DeploymentValues.AdditionalCSPPolicy))
|
||||
var cspParseErrors error
|
||||
for _, v := range api.DeploymentValues.AdditionalCSPPolicy {
|
||||
// Format is "<directive> <value> <value> ..."
|
||||
v = strings.TrimSpace(v)
|
||||
parts := strings.Split(v, " ")
|
||||
if len(parts) < 2 {
|
||||
cspParseErrors = errors.Join(cspParseErrors, xerrors.Errorf("invalid CSP header %q, not enough parts to be valid", v))
|
||||
continue
|
||||
}
|
||||
additionalCSPHeaders[httpmw.CSPFetchDirective(strings.ToLower(parts[0]))] = parts[1:]
|
||||
}
|
||||
|
||||
if cspParseErrors != nil {
|
||||
// Do not fail Coder deployment startup because of this. Just log an error
|
||||
// and continue
|
||||
api.Logger.Error(context.Background(),
|
||||
"parsing additional CSP headers", slog.Error(cspParseErrors))
|
||||
}
|
||||
|
||||
// Add CSP headers to all static assets and pages. CSP headers only affect
|
||||
// browsers, so these don't make sense on api routes.
|
||||
cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), func() []string {
|
||||
@@ -1390,7 +1411,7 @@ func New(options *Options) *API {
|
||||
}
|
||||
// By default we do not add extra websocket connections to the CSP
|
||||
return []string{}
|
||||
})
|
||||
}, additionalCSPHeaders)
|
||||
|
||||
// Static file handler must be wrapped with HSTS handler if the
|
||||
// StrictTransportSecurityAge is set. We only need to set this header on
|
||||
|
||||
+52
-38
@@ -23,29 +23,39 @@ func (s cspDirectives) Append(d CSPFetchDirective, values ...string) {
|
||||
type CSPFetchDirective string
|
||||
|
||||
const (
|
||||
cspDirectiveDefaultSrc = "default-src"
|
||||
cspDirectiveConnectSrc = "connect-src"
|
||||
cspDirectiveChildSrc = "child-src"
|
||||
cspDirectiveScriptSrc = "script-src"
|
||||
cspDirectiveFontSrc = "font-src"
|
||||
cspDirectiveStyleSrc = "style-src"
|
||||
cspDirectiveObjectSrc = "object-src"
|
||||
cspDirectiveManifestSrc = "manifest-src"
|
||||
cspDirectiveFrameSrc = "frame-src"
|
||||
cspDirectiveImgSrc = "img-src"
|
||||
cspDirectiveReportURI = "report-uri"
|
||||
cspDirectiveFormAction = "form-action"
|
||||
cspDirectiveMediaSrc = "media-src"
|
||||
cspFrameAncestors = "frame-ancestors"
|
||||
cspDirectiveWorkerSrc = "worker-src"
|
||||
CSPDirectiveDefaultSrc CSPFetchDirective = "default-src"
|
||||
CSPDirectiveConnectSrc CSPFetchDirective = "connect-src"
|
||||
CSPDirectiveChildSrc CSPFetchDirective = "child-src"
|
||||
CSPDirectiveScriptSrc CSPFetchDirective = "script-src"
|
||||
CSPDirectiveFontSrc CSPFetchDirective = "font-src"
|
||||
CSPDirectiveStyleSrc CSPFetchDirective = "style-src"
|
||||
CSPDirectiveObjectSrc CSPFetchDirective = "object-src"
|
||||
CSPDirectiveManifestSrc CSPFetchDirective = "manifest-src"
|
||||
CSPDirectiveFrameSrc CSPFetchDirective = "frame-src"
|
||||
CSPDirectiveImgSrc CSPFetchDirective = "img-src"
|
||||
CSPDirectiveReportURI CSPFetchDirective = "report-uri"
|
||||
CSPDirectiveFormAction CSPFetchDirective = "form-action"
|
||||
CSPDirectiveMediaSrc CSPFetchDirective = "media-src"
|
||||
CSPFrameAncestors CSPFetchDirective = "frame-ancestors"
|
||||
CSPDirectiveWorkerSrc CSPFetchDirective = "worker-src"
|
||||
)
|
||||
|
||||
// CSPHeaders returns a middleware that sets the Content-Security-Policy header
|
||||
// for coderd. It takes a function that allows adding supported external websocket
|
||||
// hosts. This is primarily to support the terminal connecting to a workspace proxy.
|
||||
// for coderd.
|
||||
//
|
||||
// Arguments:
|
||||
// - websocketHosts: a function that returns a list of supported external websocket hosts.
|
||||
// This is to support the terminal connecting to a workspace proxy.
|
||||
// The origin of the terminal request does not match the url of the proxy,
|
||||
// so the CSP list of allowed hosts must be dynamic and match the current
|
||||
// available proxy urls.
|
||||
// - staticAdditions: a map of CSP directives to append to the default CSP headers.
|
||||
// Used to allow specific static additions to the CSP headers. Allows some niche
|
||||
// use cases, such as embedding Coder in an iframe.
|
||||
// Example: https://github.com/coder/coder/issues/15118
|
||||
//
|
||||
//nolint:revive
|
||||
func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.Handler) http.Handler {
|
||||
func CSPHeaders(telemetry bool, websocketHosts func() []string, staticAdditions map[CSPFetchDirective][]string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Content-Security-Policy disables loading certain content types and can prevent XSS injections.
|
||||
@@ -55,30 +65,30 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
|
||||
// The list of CSP options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
|
||||
cspSrcs := cspDirectives{
|
||||
// All omitted fetch csp srcs default to this.
|
||||
cspDirectiveDefaultSrc: {"'self'"},
|
||||
cspDirectiveConnectSrc: {"'self'"},
|
||||
cspDirectiveChildSrc: {"'self'"},
|
||||
CSPDirectiveDefaultSrc: {"'self'"},
|
||||
CSPDirectiveConnectSrc: {"'self'"},
|
||||
CSPDirectiveChildSrc: {"'self'"},
|
||||
// https://github.com/suren-atoyan/monaco-react/issues/168
|
||||
cspDirectiveScriptSrc: {"'self'"},
|
||||
cspDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
|
||||
CSPDirectiveScriptSrc: {"'self'"},
|
||||
CSPDirectiveStyleSrc: {"'self' 'unsafe-inline'"},
|
||||
// data: is used by monaco editor on FE for Syntax Highlight
|
||||
cspDirectiveFontSrc: {"'self' data:"},
|
||||
cspDirectiveWorkerSrc: {"'self' blob:"},
|
||||
CSPDirectiveFontSrc: {"'self' data:"},
|
||||
CSPDirectiveWorkerSrc: {"'self' blob:"},
|
||||
// object-src is needed to support code-server
|
||||
cspDirectiveObjectSrc: {"'self'"},
|
||||
CSPDirectiveObjectSrc: {"'self'"},
|
||||
// blob: for loading the pwa manifest for code-server
|
||||
cspDirectiveManifestSrc: {"'self' blob:"},
|
||||
cspDirectiveFrameSrc: {"'self'"},
|
||||
CSPDirectiveManifestSrc: {"'self' blob:"},
|
||||
CSPDirectiveFrameSrc: {"'self'"},
|
||||
// data: for loading base64 encoded icons for generic applications.
|
||||
// https: allows loading images from external sources. This is not ideal
|
||||
// but is required for the templates page that renders readmes.
|
||||
// We should find a better solution in the future.
|
||||
cspDirectiveImgSrc: {"'self' https: data:"},
|
||||
cspDirectiveFormAction: {"'self'"},
|
||||
cspDirectiveMediaSrc: {"'self'"},
|
||||
CSPDirectiveImgSrc: {"'self' https: data:"},
|
||||
CSPDirectiveFormAction: {"'self'"},
|
||||
CSPDirectiveMediaSrc: {"'self'"},
|
||||
// Report all violations back to the server to log
|
||||
cspDirectiveReportURI: {"/api/v2/csp/reports"},
|
||||
cspFrameAncestors: {"'none'"},
|
||||
CSPDirectiveReportURI: {"/api/v2/csp/reports"},
|
||||
CSPFrameAncestors: {"'none'"},
|
||||
|
||||
// Only scripts can manipulate the dom. This prevents someone from
|
||||
// naming themselves something like '<svg onload="alert(/cross-site-scripting/)" />'.
|
||||
@@ -87,7 +97,7 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
|
||||
|
||||
if telemetry {
|
||||
// If telemetry is enabled, we report to coder.com.
|
||||
cspSrcs.Append(cspDirectiveConnectSrc, "https://coder.com")
|
||||
cspSrcs.Append(CSPDirectiveConnectSrc, "https://coder.com")
|
||||
}
|
||||
|
||||
// This extra connect-src addition is required to support old webkit
|
||||
@@ -102,7 +112,7 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
|
||||
// We can add both ws:// and wss:// as browsers do not let https
|
||||
// pages to connect to non-tls websocket connections. So this
|
||||
// supports both http & https webpages.
|
||||
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host))
|
||||
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", host))
|
||||
}
|
||||
|
||||
// The terminal requires a websocket connection to the workspace proxy.
|
||||
@@ -112,15 +122,19 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string) func(next http.H
|
||||
for _, extraHost := range extraConnect {
|
||||
if extraHost == "*" {
|
||||
// '*' means all
|
||||
cspSrcs.Append(cspDirectiveConnectSrc, "*")
|
||||
cspSrcs.Append(CSPDirectiveConnectSrc, "*")
|
||||
continue
|
||||
}
|
||||
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
|
||||
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
|
||||
// We also require this to make http/https requests to the workspace proxy for latency checking.
|
||||
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
|
||||
cspSrcs.Append(CSPDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
|
||||
}
|
||||
}
|
||||
|
||||
for directive, values := range staticAdditions {
|
||||
cspSrcs.Append(directive, values...)
|
||||
}
|
||||
|
||||
var csp strings.Builder
|
||||
for src, vals := range cspSrcs {
|
||||
_, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " "))
|
||||
|
||||
@@ -15,12 +15,15 @@ func TestCSPConnect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected := []string{"example.com", "coder.com"}
|
||||
expectedMedia := []string{"media.com", "media2.com"}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
httpmw.CSPHeaders(false, func() []string {
|
||||
return expected
|
||||
}, map[httpmw.CSPFetchDirective][]string{
|
||||
httpmw.CSPDirectiveMediaSrc: expectedMedia,
|
||||
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})).ServeHTTP(rw, r)
|
||||
@@ -30,4 +33,7 @@ func TestCSPConnect(t *testing.T) {
|
||||
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("ws://%s", e), "Content-Security-Policy header should contain ws://%s", e)
|
||||
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), fmt.Sprintf("wss://%s", e), "Content-Security-Policy header should contain wss://%s", e)
|
||||
}
|
||||
for _, e := range expectedMedia {
|
||||
require.Containsf(t, rw.Header().Get("Content-Security-Policy"), e, "Content-Security-Policy header should contain %s", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ type DeploymentValues struct {
|
||||
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
|
||||
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
|
||||
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
|
||||
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
|
||||
|
||||
Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
|
||||
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
|
||||
@@ -2147,6 +2148,18 @@ when required by your organization's security policy.`,
|
||||
Group: &deploymentGroupIntrospectionLogging,
|
||||
YAML: "enableTerraformDebugMode",
|
||||
},
|
||||
{
|
||||
Name: "Additional CSP Policy",
|
||||
Description: "Coder configures a Content Security Policy (CSP) to protect against XSS attacks. " +
|
||||
"This setting allows you to add additional CSP directives, which can open the attack surface of the deployment. " +
|
||||
"Format matches the CSP directive format, e.g. --additional-csp-policy=\"script-src https://example.com\".",
|
||||
Flag: "additional-csp-policy",
|
||||
Env: "CODER_ADDITIONAL_CSP_POLICY",
|
||||
YAML: "additionalCSPPolicy",
|
||||
Value: &c.AdditionalCSPPolicy,
|
||||
Group: &deploymentGroupNetworkingHTTP,
|
||||
},
|
||||
|
||||
// ☢️ Dangerous settings
|
||||
{
|
||||
Name: "DANGEROUS: Allow all CORS requests",
|
||||
|
||||
Generated
+1
@@ -139,6 +139,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
"scheme": "string",
|
||||
"user": {}
|
||||
},
|
||||
"additional_csp_policy": ["string"],
|
||||
"address": {
|
||||
"host": "string",
|
||||
"port": "string"
|
||||
|
||||
Generated
+3
@@ -1757,6 +1757,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"scheme": "string",
|
||||
"user": {}
|
||||
},
|
||||
"additional_csp_policy": ["string"],
|
||||
"address": {
|
||||
"host": "string",
|
||||
"port": "string"
|
||||
@@ -2181,6 +2182,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"scheme": "string",
|
||||
"user": {}
|
||||
},
|
||||
"additional_csp_policy": ["string"],
|
||||
"address": {
|
||||
"host": "string",
|
||||
"port": "string"
|
||||
@@ -2515,6 +2517,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------ |
|
||||
| `access_url` | [serpent.URL](#serpenturl) | false | | |
|
||||
| `additional_csp_policy` | array of string | false | | |
|
||||
| `address` | [serpent.HostPort](#serpenthostport) | false | | Address Use HTTPAddress or TLS.Address instead. |
|
||||
| `agent_fallback_troubleshooting_url` | [serpent.URL](#serpenturl) | false | | |
|
||||
| `agent_stat_refresh_interval` | integer | false | | |
|
||||
|
||||
Generated
+10
@@ -829,6 +829,16 @@ Output Stackdriver compatible logs to a given file.
|
||||
|
||||
Allow administrators to enable Terraform debug output.
|
||||
|
||||
### --additional-csp-policy
|
||||
|
||||
| | |
|
||||
| ----------- | ------------------------------------------------ |
|
||||
| Type | <code>string-array</code> |
|
||||
| Environment | <code>$CODER_ADDITIONAL_CSP_POLICY</code> |
|
||||
| YAML | <code>networking.http.additionalCSPPolicy</code> |
|
||||
|
||||
Coder configures a Content Security Policy (CSP) to protect against XSS attacks. This setting allows you to add additional CSP directives, which can open the attack surface of the deployment. Format matches the CSP directive format, e.g. --additional-csp-policy="script-src https://example.com".
|
||||
|
||||
### --dangerous-allow-path-app-sharing
|
||||
|
||||
| | |
|
||||
|
||||
@@ -295,6 +295,13 @@ backed by Tailscale and WireGuard.
|
||||
+ 1`. Use special value 'disable' to turn off STUN completely.
|
||||
|
||||
NETWORKING / HTTP OPTIONS:
|
||||
--additional-csp-policy string-array, $CODER_ADDITIONAL_CSP_POLICY
|
||||
Coder configures a Content Security Policy (CSP) to protect against
|
||||
XSS attacks. This setting allows you to add additional CSP directives,
|
||||
which can open the attack surface of the deployment. Format matches
|
||||
the CSP directive format, e.g. --additional-csp-policy="script-src
|
||||
https://example.com".
|
||||
|
||||
--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
|
||||
Disable password authentication. This is recommended for security
|
||||
purposes in production deployments that rely on an identity provider.
|
||||
|
||||
Generated
+1
@@ -523,6 +523,7 @@ export interface DeploymentValues {
|
||||
readonly cli_upgrade_message?: string;
|
||||
readonly terms_of_service_url?: string;
|
||||
readonly notifications?: NotificationsConfig;
|
||||
readonly additional_csp_policy?: string[];
|
||||
readonly config?: string;
|
||||
readonly write_config?: boolean;
|
||||
readonly address?: string;
|
||||
|
||||
Reference in New Issue
Block a user