diff --git a/cmd/argocd-server/commands/argocd_server.go b/cmd/argocd-server/commands/argocd_server.go index c590d29068609..c23b65ee0dbc1 100644 --- a/cmd/argocd-server/commands/argocd_server.go +++ b/cmd/argocd-server/commands/argocd_server.go @@ -61,6 +61,7 @@ func NewCommand() *cobra.Command { tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error) cacheSrc func() (*servercache.Cache, error) frameOptions string + contentSecurityPolicy string repoServerPlaintext bool repoServerStrictTLS bool staticAssetsDir string @@ -126,23 +127,24 @@ func NewCommand() *cobra.Command { } argoCDOpts := server.ArgoCDServerOpts{ - Insecure: insecure, - ListenPort: listenPort, - MetricsPort: metricsPort, - Namespace: namespace, - BaseHRef: baseHRef, - RootPath: rootPath, - KubeClientset: kubeclientset, - AppClientset: appClientSet, - RepoClientset: repoclientset, - DexServerAddr: dexServerAddress, - DisableAuth: disableAuth, - EnableGZip: enableGZip, - TLSConfigCustomizer: tlsConfigCustomizer, - Cache: cache, - XFrameOptions: frameOptions, - RedisClient: redisClient, - StaticAssetsDir: staticAssetsDir, + Insecure: insecure, + ListenPort: listenPort, + MetricsPort: metricsPort, + Namespace: namespace, + BaseHRef: baseHRef, + RootPath: rootPath, + KubeClientset: kubeclientset, + AppClientset: appClientSet, + RepoClientset: repoclientset, + DexServerAddr: dexServerAddress, + DisableAuth: disableAuth, + EnableGZip: enableGZip, + TLSConfigCustomizer: tlsConfigCustomizer, + Cache: cache, + XFrameOptions: frameOptions, + ContentSecurityPolicy: contentSecurityPolicy, + RedisClient: redisClient, + StaticAssetsDir: staticAssetsDir, } stats.RegisterStackDumper() @@ -176,6 +178,7 @@ func NewCommand() *cobra.Command { command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port") command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.") command.Flags().StringVar(&frameOptions, "x-frame-options", env.StringFromEnv("ARGOCD_SERVER_X_FRAME_OPTIONS", "sameorigin"), "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".") + command.Flags().StringVar(&contentSecurityPolicy, "content-security-policy", env.StringFromEnv("ARGOCD_SERVER_CONTENT_SECURITY_POLICY", "frame-ancestors 'self';"), "Set Content-Security-Policy header in HTTP responses to `value`. To disable, set to \"\".") command.Flags().BoolVar(&repoServerPlaintext, "repo-server-plaintext", env.ParseBoolFromEnv("ARGOCD_SERVER_REPO_SERVER_PLAINTEXT", false), "Use a plaintext client (non-TLS) to connect to repository server") command.Flags().BoolVar(&repoServerStrictTLS, "repo-server-strict-tls", env.ParseBoolFromEnv("ARGOCD_SERVER_REPO_SERVER_STRICT_TLS", false), "Perform strict validation of TLS certificates when connecting to repo server") tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command) diff --git a/docs/operator-manual/server-commands/argocd-server.md b/docs/operator-manual/server-commands/argocd-server.md index 3d83f3f79b4ca..369c142310aad 100644 --- a/docs/operator-manual/server-commands/argocd-server.md +++ b/docs/operator-manual/server-commands/argocd-server.md @@ -23,6 +23,7 @@ argocd-server [flags] --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --connection-status-cache-expiration duration Cache expiration for cluster/repo connection status (default 1h0m0s) + --content-security-policy value Set Content-Security-Policy header in HTTP responses to value. To disable, set to "". (default "frame-ancestors 'self';") --context string The name of the kubeconfig context to use --default-cache-expiration duration Cache expiration default (default 24h0m0s) --dex-server string Dex server address (default "http://argocd-dex-server:5556") diff --git a/manifests/base/server/argocd-server-deployment.yaml b/manifests/base/server/argocd-server-deployment.yaml index 604340b794e7d..beeebf1cb7ee9 100644 --- a/manifests/base/server/argocd-server-deployment.yaml +++ b/manifests/base/server/argocd-server-deployment.yaml @@ -88,6 +88,12 @@ spec: name: argocd-cmd-params-cm key: server.x.frame.options optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.content.security.policy + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: configMapKeyRef: diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index fc8ca277254c5..a67c2339f3866 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -10480,6 +10480,12 @@ spec: key: server.x.frame.options name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + key: server.content.security.policy + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: configMapKeyRef: diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml index 4aa100e20da7e..1731c71516215 100644 --- a/manifests/ha/namespace-install.yaml +++ b/manifests/ha/namespace-install.yaml @@ -1767,6 +1767,12 @@ spec: key: server.x.frame.options name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + key: server.content.security.policy + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: configMapKeyRef: diff --git a/manifests/install.yaml b/manifests/install.yaml index fc5c4562dab97..ced47d24e1e75 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -9810,6 +9810,12 @@ spec: key: server.x.frame.options name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + key: server.content.security.policy + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: configMapKeyRef: diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 8918b52e64db5..76d48860dbda0 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -1097,6 +1097,12 @@ spec: key: server.x.frame.options name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + key: server.content.security.policy + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: configMapKeyRef: diff --git a/server/server.go b/server/server.go index 921f47ce09463..5a273f005ca61 100644 --- a/server/server.go +++ b/server/server.go @@ -171,24 +171,25 @@ type ArgoCDServer struct { } type ArgoCDServerOpts struct { - DisableAuth bool - EnableGZip bool - Insecure bool - StaticAssetsDir string - ListenPort int - MetricsPort int - Namespace string - DexServerAddr string - BaseHRef string - RootPath string - KubeClientset kubernetes.Interface - AppClientset appclientset.Interface - RepoClientset repoapiclient.Clientset - Cache *servercache.Cache - RedisClient *redis.Client - TLSConfigCustomizer tlsutil.ConfigCustomizer - XFrameOptions string - ListenHost string + DisableAuth bool + EnableGZip bool + Insecure bool + StaticAssetsDir string + ListenPort int + MetricsPort int + Namespace string + DexServerAddr string + BaseHRef string + RootPath string + KubeClientset kubernetes.Interface + AppClientset appclientset.Interface + RepoClientset repoapiclient.Clientset + Cache *servercache.Cache + RedisClient *redis.Client + TLSConfigCustomizer tlsutil.ConfigCustomizer + XFrameOptions string + ContentSecurityPolicy string + ListenHost string } // initializeDefaultProject creates the default project if it does not already exist @@ -886,6 +887,10 @@ func (server *ArgoCDServer) newStaticAssetsHandler() func(http.ResponseWriter, * if server.XFrameOptions != "" { w.Header().Set("X-Frame-Options", server.XFrameOptions) } + // Set Content-Security-Policy according to configuration + if server.ContentSecurityPolicy != "" { + w.Header().Set("Content-Security-Policy", server.ContentSecurityPolicy) + } w.Header().Set("X-XSS-Protection", "1") // serve index.html for non file requests to support HTML5 History API diff --git a/server/server_norace_test.go b/server/server_norace_test.go index 0b7ae0acdf1f8..1938486d13c49 100644 --- a/server/server_norace_test.go +++ b/server/server_norace_test.go @@ -101,7 +101,7 @@ func Test_StaticHeaders(t *testing.T) { // !race: // Same as TestUserAgent - // Test default policy "sameorigin" + // Test default policy "sameorigin" and "frame-ancestors 'self';" { s, closer := fakeServer() defer closer() @@ -129,13 +129,15 @@ func Test_StaticHeaders(t *testing.T) { resp, err := client.Do(req) assert.NoError(t, err) assert.Equal(t, "sameorigin", resp.Header.Get("X-Frame-Options")) + assert.Equal(t, "frame-ancestors 'self';", resp.Header.Get("Content-Security-Policy")) } - // Test custom policy + // Test custom policy for X-Frame-Options and Content-Security-Policy { s, closer := fakeServer() defer closer() s.XFrameOptions = "deny" + s.ContentSecurityPolicy = "frame-ancestors 'none';" cancelInformer := test.StartInformer(s.projInformer) defer cancelInformer() port, err := test.GetFreePort() @@ -160,13 +162,15 @@ func Test_StaticHeaders(t *testing.T) { resp, err := client.Do(req) assert.NoError(t, err) assert.Equal(t, "deny", resp.Header.Get("X-Frame-Options")) + assert.Equal(t, "frame-ancestors 'none';", resp.Header.Get("Content-Security-Policy")) } - // Test disabled + // Test disabled X-Frame-Options and Content-Security-Policy { s, closer := fakeServer() defer closer() s.XFrameOptions = "" + s.ContentSecurityPolicy = "" cancelInformer := test.StartInformer(s.projInformer) defer cancelInformer() port, err := test.GetFreePort() @@ -191,5 +195,6 @@ func Test_StaticHeaders(t *testing.T) { resp, err := client.Do(req) assert.NoError(t, err) assert.Empty(t, resp.Header.Get("X-Frame-Options")) + assert.Empty(t, resp.Header.Get("Content-Security-Policy")) } } diff --git a/server/server_test.go b/server/server_test.go index fba450acb5390..59eaa4f4e3564 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -39,12 +39,13 @@ func fakeServer() (*ArgoCDServer, func()) { redis, closer := test.NewInMemoryRedis() argoCDOpts := ArgoCDServerOpts{ - Namespace: test.FakeArgoCDNamespace, - KubeClientset: kubeclientset, - AppClientset: appClientSet, - Insecure: true, - DisableAuth: true, - XFrameOptions: "sameorigin", + Namespace: test.FakeArgoCDNamespace, + KubeClientset: kubeclientset, + AppClientset: appClientSet, + Insecure: true, + DisableAuth: true, + XFrameOptions: "sameorigin", + ContentSecurityPolicy: "frame-ancestors 'self';", Cache: servercache.NewCache( appstatecache.NewCache( cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Hour)),