diff --git a/Dockerfile b/Dockerfile index a35ad80d18dd5..90c411b22c403 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ WORKDIR /home/argocd #################################################################################################### # Argo CD UI stage #################################################################################################### -FROM --platform=$BUILDPLATFORM docker.io/library/node:20.4.0@sha256:b3ca7d32f0c12291df6e45a914d4ee60011a3fce4a978df5e609e356a4a2cb88 AS argocd-ui +FROM --platform=$BUILDPLATFORM docker.io/library/node:20.5.0@sha256:32ec50b65ac9572eda92baa6004a04dbbfc8021ea806fa62d37336183cad04e6 AS argocd-ui WORKDIR /src COPY ["ui/package.json", "ui/yarn.lock", "./"] diff --git a/Makefile b/Makefile index d75fd364e31d9..4c119188105b9 100644 --- a/Makefile +++ b/Makefile @@ -460,6 +460,7 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local BIN_MODE=$(ARGOCD_BIN_MODE) \ ARGOCD_APPLICATION_NAMESPACES=argocd-e2e-external \ ARGOCD_APPLICATIONSET_CONTROLLER_NAMESPACES=argocd-e2e-external \ + ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS=http://127.0.0.1:8341,http://127.0.0.1:8342,http://127.0.0.1:8343,http://127.0.0.1:8344 \ ARGOCD_E2E_TEST=true \ goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START} diff --git a/SECURITY.md b/SECURITY.md index 9e2ba5c6ba542..38574aa2bd0db 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,9 +35,7 @@ impact on Argo CD before opening an issue at least roughly. ## Supported Versions -We currently support the most recent release (`N`, e.g. `1.8`) and the release -previous to the most recent one (`N-1`, e.g. `1.7`). With the release of -`N+1`, `N-1` drops out of support and `N` becomes `N-1`. +We currently support the last 3 minor versions of Argo CD with security and bug fixes. We regularly perform patch releases (e.g. `1.8.5` and `1.7.12`) for the supported versions, which will contain fixes for security vulnerabilities and diff --git a/USERS.md b/USERS.md index 5d83fde59ed32..db1a5f0da6ff5 100644 --- a/USERS.md +++ b/USERS.md @@ -24,6 +24,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [AppDirect](https://www.appdirect.com) 1. [Arctiq Inc.](https://www.arctiq.ca) 1. [ARZ Allgemeines Rechenzentrum GmbH](https://www.arz.at/) +2. [Autodesk](https://www.autodesk.com) 1. [Axual B.V.](https://axual.com) 1. [Back Market](https://www.backmarket.com) 1. [Baloise](https://www.baloise.com) diff --git a/applicationset/controllers/requeue_after_test.go b/applicationset/controllers/requeue_after_test.go index a831b70ed2275..da6b0b10b47df 100644 --- a/applicationset/controllers/requeue_after_test.go +++ b/applicationset/controllers/requeue_after_test.go @@ -60,9 +60,9 @@ func TestRequeueAfter(t *testing.T) { "List": generators.NewListGenerator(), "Clusters": generators.NewClusterGenerator(k8sClient, ctx, appClientset, "argocd"), "Git": generators.NewGitGenerator(mockServer), - "SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, ""), + "SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, "", []string{""}), "ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd"), - "PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, ""), + "PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, "", []string{""}), } nestedGenerators := map[string]generators.Generator{ diff --git a/applicationset/generators/pull_request.go b/applicationset/generators/pull_request.go index d861010daa65e..c024f1b723919 100644 --- a/applicationset/generators/pull_request.go +++ b/applicationset/generators/pull_request.go @@ -26,13 +26,15 @@ type PullRequestGenerator struct { selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) auth SCMAuthProviders scmRootCAPath string + allowedSCMProviders []string } -func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string) Generator { +func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string, allowedScmProviders []string) Generator { g := &PullRequestGenerator{ - client: client, - auth: auth, - scmRootCAPath: scmRootCAPath, + client: client, + auth: auth, + scmRootCAPath: scmRootCAPath, + allowedSCMProviders: allowedScmProviders, } g.selectServiceProviderFunc = g.selectServiceProvider return g @@ -120,10 +122,16 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha // selectServiceProvider selects the provider to get pull requests from the configuration func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) { if generatorConfig.Github != nil { + if !ScmProviderAllowed(applicationSetInfo, generatorConfig.Github.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", generatorConfig.Github.API) + } return g.github(ctx, generatorConfig.Github, applicationSetInfo) } if generatorConfig.GitLab != nil { providerConfig := generatorConfig.GitLab + if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API) + } token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace) if err != nil { return nil, fmt.Errorf("error fetching Secret token: %v", err) @@ -132,6 +140,9 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera } if generatorConfig.Gitea != nil { providerConfig := generatorConfig.Gitea + if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", generatorConfig.Gitea.API) + } token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace) if err != nil { return nil, fmt.Errorf("error fetching Secret token: %v", err) @@ -140,6 +151,9 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera } if generatorConfig.BitbucketServer != nil { providerConfig := generatorConfig.BitbucketServer + if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API) + } if providerConfig.BasicAuth != nil { password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace) if err != nil { diff --git a/applicationset/generators/pull_request_test.go b/applicationset/generators/pull_request_test.go index eb0b3bcdd8a90..72017f522946e 100644 --- a/applicationset/generators/pull_request_test.go +++ b/applicationset/generators/pull_request_test.go @@ -273,3 +273,80 @@ func TestPullRequestGetSecretRef(t *testing.T) { }) } } + +func TestAllowedSCMProviderPullRequest(t *testing.T) { + cases := []struct { + name string + providerConfig *argoprojiov1alpha1.PullRequestGenerator + expectedError string + }{ + { + name: "Error Github", + providerConfig: &argoprojiov1alpha1.PullRequestGenerator{ + Github: &argoprojiov1alpha1.PullRequestGeneratorGithub{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Gitlab", + providerConfig: &argoprojiov1alpha1.PullRequestGenerator{ + GitLab: &argoprojiov1alpha1.PullRequestGeneratorGitLab{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Gitea", + providerConfig: &argoprojiov1alpha1.PullRequestGenerator{ + Gitea: &argoprojiov1alpha1.PullRequestGeneratorGitea{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Bitbucket", + providerConfig: &argoprojiov1alpha1.PullRequestGenerator{ + BitbucketServer: &argoprojiov1alpha1.PullRequestGeneratorBitbucketServer{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + } + + for _, testCase := range cases { + testCaseCopy := testCase + + t.Run(testCaseCopy.name, func(t *testing.T) { + t.Parallel() + + pullRequestGenerator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{ + "github.myorg.com", + "gitlab.myorg.com", + "gitea.myorg.com", + "bitbucket.myorg.com", + "azuredevops.myorg.com", + }) + + applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "set", + }, + Spec: argoprojiov1alpha1.ApplicationSetSpec{ + Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{ + PullRequest: testCaseCopy.providerConfig, + }}, + }, + } + + _, err := pullRequestGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo) + + assert.Error(t, err, "Must return an error") + assert.Equal(t, testCaseCopy.expectedError, err.Error()) + }) + } +} diff --git a/applicationset/generators/scm_provider.go b/applicationset/generators/scm_provider.go index 34742f4822ef8..67ea279b3fb39 100644 --- a/applicationset/generators/scm_provider.go +++ b/applicationset/generators/scm_provider.go @@ -9,9 +9,12 @@ import ( corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + log "github.com/sirupsen/logrus" + "github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth" "github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider" "github.com/argoproj/argo-cd/v2/applicationset/utils" + "github.com/argoproj/argo-cd/v2/common" argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" ) @@ -26,18 +29,20 @@ type SCMProviderGenerator struct { // Testing hooks. overrideProvider scm_provider.SCMProviderService SCMAuthProviders - scmRootCAPath string + scmRootCAPath string + allowedSCMProviders []string } type SCMAuthProviders struct { GitHubApps github_app_auth.Credentials } -func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string) Generator { +func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string, allowedSCMProviders []string) Generator { return &SCMProviderGenerator{ - client: client, - SCMAuthProviders: providers, - scmRootCAPath: scmRootCAPath, + client: client, + SCMAuthProviders: providers, + scmRootCAPath: scmRootCAPath, + allowedSCMProviders: allowedSCMProviders, } } @@ -60,6 +65,26 @@ func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.A return &appSetGenerator.SCMProvider.Template } +func ScmProviderAllowed(applicationSetInfo *argoprojiov1alpha1.ApplicationSet, url string, allowedScmProviders []string) bool { + if url == "" || len(allowedScmProviders) == 0 { + return true + } + + for _, allowedScmProvider := range allowedScmProviders { + if url == allowedScmProvider { + return true + } + } + + log.WithFields(log.Fields{ + common.SecurityField: common.SecurityMedium, + "applicationset": applicationSetInfo.Name, + "appSetNamespace": applicationSetInfo.Namespace, + }).Debugf("attempted to use disallowed SCM %q", url) + + return false +} + func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) { if appSetGenerator == nil { return nil, EmptyAppSetGeneratorError @@ -77,12 +102,18 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha if g.overrideProvider != nil { provider = g.overrideProvider } else if providerConfig.Github != nil { + if !ScmProviderAllowed(applicationSetInfo, providerConfig.Github.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Github.API) + } var err error provider, err = g.githubProvider(ctx, providerConfig.Github, applicationSetInfo) if err != nil { return nil, fmt.Errorf("scm provider: %w", err) } } else if providerConfig.Gitlab != nil { + if !ScmProviderAllowed(applicationSetInfo, providerConfig.Gitlab.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Gitlab.API) + } token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace) if err != nil { return nil, fmt.Errorf("error fetching Gitlab token: %v", err) @@ -92,6 +123,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha return nil, fmt.Errorf("error initializing Gitlab service: %v", err) } } else if providerConfig.Gitea != nil { + if !ScmProviderAllowed(applicationSetInfo, providerConfig.Gitea.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Gitea.API) + } token, err := g.getSecretRef(ctx, providerConfig.Gitea.TokenRef, applicationSetInfo.Namespace) if err != nil { return nil, fmt.Errorf("error fetching Gitea token: %v", err) @@ -102,6 +136,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha } } else if providerConfig.BitbucketServer != nil { providerConfig := providerConfig.BitbucketServer + if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API) + } var scmError error if providerConfig.BasicAuth != nil { password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace) @@ -116,6 +153,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha return nil, fmt.Errorf("error initializing Bitbucket Server service: %v", scmError) } } else if providerConfig.AzureDevOps != nil { + if !ScmProviderAllowed(applicationSetInfo, providerConfig.AzureDevOps.API, g.allowedSCMProviders) { + return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.AzureDevOps.API) + } token, err := g.getSecretRef(ctx, providerConfig.AzureDevOps.AccessTokenRef, applicationSetInfo.Namespace) if err != nil { return nil, fmt.Errorf("error fetching Azure Devops access token: %v", err) diff --git a/applicationset/generators/scm_provider_test.go b/applicationset/generators/scm_provider_test.go index d51cb2703ad7f..4dcb8fdf3ce6f 100644 --- a/applicationset/generators/scm_provider_test.go +++ b/applicationset/generators/scm_provider_test.go @@ -200,3 +200,89 @@ func TestSCMProviderGenerateParams(t *testing.T) { }) } } + +func TestAllowedSCMProvider(t *testing.T) { + cases := []struct { + name string + providerConfig *argoprojiov1alpha1.SCMProviderGenerator + expectedError string + }{ + { + name: "Error Github", + providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{ + Github: &argoprojiov1alpha1.SCMProviderGeneratorGithub{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Gitlab", + providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{ + Gitlab: &argoprojiov1alpha1.SCMProviderGeneratorGitlab{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Gitea", + providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{ + Gitea: &argoprojiov1alpha1.SCMProviderGeneratorGitea{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error Bitbucket", + providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{ + BitbucketServer: &argoprojiov1alpha1.SCMProviderGeneratorBitbucketServer{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + { + name: "Error AzureDevops", + providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{ + AzureDevOps: &argoprojiov1alpha1.SCMProviderGeneratorAzureDevOps{ + API: "https://myservice.mynamespace.svc.cluster.local", + }, + }, + expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local", + }, + } + + for _, testCase := range cases { + testCaseCopy := testCase + + t.Run(testCaseCopy.name, func(t *testing.T) { + t.Parallel() + + scmGenerator := &SCMProviderGenerator{allowedSCMProviders: []string{ + "github.myorg.com", + "gitlab.myorg.com", + "gitea.myorg.com", + "bitbucket.myorg.com", + "azuredevops.myorg.com", + }} + + applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "set", + }, + Spec: argoprojiov1alpha1.ApplicationSetSpec{ + Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{ + SCMProvider: testCaseCopy.providerConfig, + }}, + }, + } + + _, err := scmGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo) + + assert.Error(t, err, "Must return an error") + assert.Equal(t, testCaseCopy.expectedError, err.Error()) + }) + } +} diff --git a/assets/swagger.json b/assets/swagger.json index 38d98c3460b35..1c724f649504d 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -8036,6 +8036,12 @@ "disabled": { "type": "boolean" }, + "displayName": { + "type": "string" + }, + "iconClass": { + "type": "string" + }, "name": { "type": "string" }, diff --git a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go index f873c912d4f73..1226202de7e01 100644 --- a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go +++ b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go @@ -65,6 +65,7 @@ func NewCommand() *cobra.Command { repoServerTimeoutSeconds int maxConcurrentReconciliations int scmRootCAPath string + allowedScmProviders []string ) scheme := runtime.NewScheme() _ = clientgoscheme.AddToScheme(scheme) @@ -97,7 +98,7 @@ func NewCommand() *cobra.Command { policyObj, exists := utils.Policies[policy] if !exists { - log.Info("Policy value can be: sync, create-only, create-update, create-delete, default value: sync") + log.Error("Policy value can be: sync, create-only, create-update, create-delete, default value: sync") os.Exit(1) } @@ -107,6 +108,9 @@ func NewCommand() *cobra.Command { // If the applicationset-namespaces contains only one namespace it corresponds to the current namespace if len(applicationSetNamespaces) == 1 { watchedNamespace = (applicationSetNamespaces)[0] + } else if len(allowedScmProviders) == 0 { + log.Error("When enabling applicationset in any namespace using applicationset-namespaces, allowed-scm-providers is required") + os.Exit(1) } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ @@ -159,9 +163,9 @@ func NewCommand() *cobra.Command { "List": generators.NewListGenerator(), "Clusters": generators.NewClusterGenerator(mgr.GetClient(), ctx, k8sClient, namespace), "Git": generators.NewGitGenerator(argoCDService), - "SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient(), scmAuth, scmRootCAPath), + "SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient(), scmAuth, scmRootCAPath, allowedScmProviders), "ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace), - "PullRequest": generators.NewPullRequestGenerator(mgr.GetClient(), scmAuth, scmRootCAPath), + "PullRequest": generators.NewPullRequestGenerator(mgr.GetClient(), scmAuth, scmRootCAPath, allowedScmProviders), "Plugin": generators.NewPluginGenerator(mgr.GetClient(), ctx, k8sClient, namespace), } @@ -241,6 +245,7 @@ func NewCommand() *cobra.Command { command.Flags().BoolVar(&debugLog, "debug", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_DEBUG", false), "Print debug logs. Takes precedence over loglevel") command.Flags().StringVar(&cmdutil.LogFormat, "logformat", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_LOGFORMAT", "text"), "Set the logging format. One of: text|json") command.Flags().StringVar(&cmdutil.LogLevel, "loglevel", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_LOGLEVEL", "info"), "Set the logging level. One of: debug|info|warn|error") + command.Flags().StringSliceVar(&allowedScmProviders, "allowed-scm-providers", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS", []string{}, ","), "The list of allowed scm providers. (Default: Empty = all)") command.Flags().BoolVar(&dryRun, "dry-run", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_DRY_RUN", false), "Enable dry run mode") command.Flags().BoolVar(&enableProgressiveSyncs, "enable-progressive-syncs", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS", false), "Enable use of the experimental progressive syncs feature.") command.Flags().BoolVar(&enableNewGitFileGlobbing, "enable-new-git-file-globbing", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING", false), "Enable new globbing in Git files generator.") diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 45f1b0d4e9e90..9f3e32ba0b6e6 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1248,40 +1248,44 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli } func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) { - kube.RetryUntilSucceed(context.Background(), updateOperationStateTimeout, "Update application operation state", logutils.NewLogrusLogger(logutils.NewWithCurrentConfig()), func() error { - if state.Phase == "" { - // expose any bugs where we neglect to set phase - panic("no phase was set") - } - if state.Phase.Completed() { - now := metav1.Now() - state.FinishedAt = &now - } - patch := map[string]interface{}{ - "status": map[string]interface{}{ - "operationState": state, - }, - } - if state.Phase.Completed() { - // If operation is completed, clear the operation field to indicate no operation is - // in progress. - patch["operation"] = nil - } - if reflect.DeepEqual(app.Status.OperationState, state) { - log.Infof("No operation updates necessary to '%s'. Skipping patch", app.QualifiedName()) - return nil - } - patchJSON, err := json.Marshal(patch) + logCtx := log.WithFields(log.Fields{"application": app.Name, "appNamespace": app.Namespace, "project": app.Spec.Project}) + + if state.Phase == "" { + // expose any bugs where we neglect to set phase + panic("no phase was set") + } + if state.Phase.Completed() { + now := metav1.Now() + state.FinishedAt = &now + } + patch := map[string]interface{}{ + "status": map[string]interface{}{ + "operationState": state, + }, + } + if state.Phase.Completed() { + // If operation is completed, clear the operation field to indicate no operation is + // in progress. + patch["operation"] = nil + } + if reflect.DeepEqual(app.Status.OperationState, state) { + logCtx.Infof("No operation updates necessary to '%s'. Skipping patch", app.QualifiedName()) + return + } + patchJSON, err := json.Marshal(patch) + if err != nil { + logCtx.Errorf("error marshaling json: %v", err) + return + } + if app.Status.OperationState != nil && app.Status.OperationState.FinishedAt != nil && state.FinishedAt == nil { + patchJSON, err = jsonpatch.MergeMergePatches(patchJSON, []byte(`{"status": {"operationState": {"finishedAt": null}}}`)) if err != nil { - return fmt.Errorf("error marshaling json: %w", err) - } - if app.Status.OperationState != nil && app.Status.OperationState.FinishedAt != nil && state.FinishedAt == nil { - patchJSON, err = jsonpatch.MergeMergePatches(patchJSON, []byte(`{"status": {"operationState": {"finishedAt": null}}}`)) - if err != nil { - return fmt.Errorf("error merging operation state patch: %w", err) - } + logCtx.Errorf("error merging operation state patch: %v", err) + return } + } + kube.RetryUntilSucceed(context.Background(), updateOperationStateTimeout, "Update application operation state", logutils.NewLogrusLogger(logutils.NewWithCurrentConfig()), func() error { appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace) _, err = appClient.Patch(context.Background(), app.Name, types.MergePatchType, patchJSON, metav1.PatchOptions{}) if err != nil { @@ -1289,32 +1293,36 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta if apierr.IsNotFound(err) { return nil } + // kube.RetryUntilSucceed logs failed attempts at "debug" level, but we want to know if this fails. Log a + // warning. + logCtx.Warnf("error patching application with operation state: %v", err) return fmt.Errorf("error patching application with operation state: %w", err) } - log.Infof("updated '%s' operation (phase: %s)", app.QualifiedName(), state.Phase) - if state.Phase.Completed() { - eventInfo := argo.EventInfo{Reason: argo.EventReasonOperationCompleted} - var messages []string - if state.Operation.Sync != nil && len(state.Operation.Sync.Resources) > 0 { - messages = []string{"Partial sync operation"} - } else { - messages = []string{"Sync operation"} - } - if state.SyncResult != nil { - messages = append(messages, "to", state.SyncResult.Revision) - } - if state.Phase.Successful() { - eventInfo.Type = v1.EventTypeNormal - messages = append(messages, "succeeded") - } else { - eventInfo.Type = v1.EventTypeWarning - messages = append(messages, "failed:", state.Message) - } - ctrl.auditLogger.LogAppEvent(app, eventInfo, strings.Join(messages, " "), "") - ctrl.metricsServer.IncSync(app, state) - } return nil }) + + logCtx.Infof("updated '%s' operation (phase: %s)", app.QualifiedName(), state.Phase) + if state.Phase.Completed() { + eventInfo := argo.EventInfo{Reason: argo.EventReasonOperationCompleted} + var messages []string + if state.Operation.Sync != nil && len(state.Operation.Sync.Resources) > 0 { + messages = []string{"Partial sync operation"} + } else { + messages = []string{"Sync operation"} + } + if state.SyncResult != nil { + messages = append(messages, "to", state.SyncResult.Revision) + } + if state.Phase.Successful() { + eventInfo.Type = v1.EventTypeNormal + messages = append(messages, "succeeded") + } else { + eventInfo.Type = v1.EventTypeWarning + messages = append(messages, "failed:", state.Message) + } + ctrl.auditLogger.LogAppEvent(app, eventInfo, strings.Join(messages, " "), "") + ctrl.metricsServer.IncSync(app, state) + } } func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) { diff --git a/controller/appcontroller_test.go b/controller/appcontroller_test.go index a849c3bd292ca..f43e1329680fa 100644 --- a/controller/appcontroller_test.go +++ b/controller/appcontroller_test.go @@ -3,9 +3,11 @@ package controller import ( "context" "encoding/json" + "errors" "testing" "time" + "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/resource" clustercache "github.com/argoproj/gitops-engine/pkg/cache" @@ -926,6 +928,41 @@ func TestSetOperationStateOnDeletedApp(t *testing.T) { assert.True(t, patched) } +type logHook struct { + entries []logrus.Entry +} + +func (h *logHook) Levels() []logrus.Level { + return []logrus.Level{logrus.WarnLevel} +} + +func (h *logHook) Fire(entry *logrus.Entry) error { + h.entries = append(h.entries, *entry) + return nil +} + +func TestSetOperationStateLogRetries(t *testing.T) { + hook := logHook{} + logrus.AddHook(&hook) + t.Cleanup(func() { + logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) + }) + ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}) + fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) + fakeAppCs.ReactionChain = nil + patched := false + fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if !patched { + patched = true + return true, nil, errors.New("fake error") + } + return true, nil, nil + }) + ctrl.setOperationState(newFakeApp(), &v1alpha1.OperationState{Phase: synccommon.OperationSucceeded}) + assert.True(t, patched) + assert.Contains(t, hook.entries[0].Message, "fake error") +} + func TestNeedRefreshAppStatus(t *testing.T) { testCases := []struct { name string diff --git a/controller/cache/cache.go b/controller/cache/cache.go index 8623130f36dfa..b62185dc4589f 100644 --- a/controller/cache/cache.go +++ b/controller/cache/cache.go @@ -775,12 +775,14 @@ func (c *liveStateCache) handleModEvent(oldCluster *appv1.Cluster, newCluster *a } func (c *liveStateCache) handleDeleteEvent(clusterServer string) { - c.lock.Lock() - defer c.lock.Unlock() + c.lock.RLock() cluster, ok := c.clusters[clusterServer] + c.lock.RUnlock() if ok { cluster.Invalidate() + c.lock.Lock() delete(c.clusters, clusterServer) + c.lock.Unlock() } } diff --git a/controller/cache/cache_test.go b/controller/cache/cache_test.go index 3549f03f6e0ea..de2d96eb7aa28 100644 --- a/controller/cache/cache_test.go +++ b/controller/cache/cache_test.go @@ -1,13 +1,16 @@ package cache import ( + "context" "errors" "net" "net/url" + "sync" "testing" + "time" "github.com/stretchr/testify/assert" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -16,8 +19,10 @@ import ( "github.com/argoproj/gitops-engine/pkg/cache/mocks" "github.com/argoproj/gitops-engine/pkg/health" "github.com/stretchr/testify/mock" + "k8s.io/client-go/kubernetes/fake" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + argosettings "github.com/argoproj/argo-cd/v2/util/settings" ) type netError string @@ -108,6 +113,98 @@ func TestHandleAddEvent_ClusterExcluded(t *testing.T) { assert.Len(t, clustersCache.clusters, 0) } +func TestHandleDeleteEvent_CacheDeadlock(t *testing.T) { + testCluster := &appv1.Cluster{ + Server: "https://mycluster", + Config: appv1.ClusterConfig{Username: "bar"}, + } + fakeClient := fake.NewSimpleClientset() + settingsMgr := argosettings.NewSettingsManager(context.TODO(), fakeClient, "argocd") + externalLockRef := sync.RWMutex{} + gitopsEngineClusterCache := &mocks.ClusterCache{} + clustersCache := liveStateCache{ + clusters: map[string]cache.ClusterCache{ + testCluster.Server: gitopsEngineClusterCache, + }, + clusterFilter: func(cluster *appv1.Cluster) bool { + return true + }, + settingsMgr: settingsMgr, + // Set the lock here so we can reference it later + // nolint We need to overwrite here to have access to the lock + lock: externalLockRef, + } + channel := make(chan string) + // Mocked lock held by the gitops-engine cluster cache + mockMutex := sync.RWMutex{} + // Locks to force trigger condition during test + // Condition order: + // EnsuredSynced -> Locks gitops-engine + // handleDeleteEvent -> Locks liveStateCache + // EnsureSynced via sync, newResource, populateResourceInfoHandler -> attempts to Lock liveStateCache + // handleDeleteEvent via cluster.Invalidate -> attempts to Lock gitops-engine + handleDeleteWasCalled := sync.Mutex{} + engineHoldsLock := sync.Mutex{} + handleDeleteWasCalled.Lock() + engineHoldsLock.Lock() + gitopsEngineClusterCache.On("EnsureSynced").Run(func(args mock.Arguments) { + // Held by EnsureSync calling into sync and watchEvents + mockMutex.Lock() + defer mockMutex.Unlock() + // Continue Execution of timer func + engineHoldsLock.Unlock() + // Wait for handleDeleteEvent to be called triggering the lock + // on the liveStateCache + handleDeleteWasCalled.Lock() + t.Logf("handleDelete was called, EnsureSynced continuing...") + handleDeleteWasCalled.Unlock() + // Try and obtain the lock on the liveStateCache + alreadyFailed := !externalLockRef.TryLock() + if alreadyFailed { + channel <- "DEADLOCKED -- EnsureSynced could not obtain lock on liveStateCache" + return + } + externalLockRef.Lock() + t.Logf("EnsureSynce was able to lock liveStateCache") + externalLockRef.Unlock() + }).Return(nil).Once() + gitopsEngineClusterCache.On("Invalidate").Run(func(args mock.Arguments) { + // If deadlock is fixed should be able to acquire lock here + alreadyFailed := !mockMutex.TryLock() + if alreadyFailed { + channel <- "DEADLOCKED -- Invalidate could not obtain lock on gitops-engine" + return + } + mockMutex.Lock() + t.Logf("Invalidate was able to lock gitops-engine cache") + mockMutex.Unlock() + }).Return() + go func() { + // Start the gitops-engine lock holds + go func() { + err := gitopsEngineClusterCache.EnsureSynced() + if err != nil { + assert.Fail(t, err.Error()) + } + }() + // Wait for EnsureSynced to grab the lock for gitops-engine + engineHoldsLock.Lock() + t.Log("EnsureSynced has obtained lock on gitops-engine") + engineHoldsLock.Unlock() + // Run in background + go clustersCache.handleDeleteEvent(testCluster.Server) + // Allow execution to continue on clusters cache call to trigger lock + handleDeleteWasCalled.Unlock() + channel <- "PASSED" + }() + select { + case str := <-channel: + assert.Equal(t, "PASSED", str, str) + case <-time.After(5 * time.Second): + assert.Fail(t, "Ended up in deadlock") + } +} + func TestIsRetryableError(t *testing.T) { var ( tlsHandshakeTimeoutErr net.Error = netError("net/http: TLS handshake timeout") diff --git a/controller/metrics/metrics.go b/controller/metrics/metrics.go index 3cd9837ff7036..3cfb16a249339 100644 --- a/controller/metrics/metrics.go +++ b/controller/metrics/metrics.go @@ -56,7 +56,7 @@ var ( descAppInfo = prometheus.NewDesc( "argocd_app_info", "Information about application.", - append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"), + append(descAppDefaultLabels, "autosync_enabled", "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"), nil, ) // DEPRECATED @@ -381,7 +381,9 @@ func (c *appCollector) collectApps(ch chan<- prometheus.Metric, app *argoappv1.A healthStatus = health.HealthStatusUnknown } - addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.GetSource().RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), string(healthStatus), operation) + autoSyncEnabled := app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil + + addGauge(descAppInfo, 1, strconv.FormatBool(autoSyncEnabled), git.NormalizeGitURL(app.Spec.GetSource().RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), string(healthStatus), operation) if len(c.appLabels) > 0 { labelValues := []string{} diff --git a/controller/metrics/metrics_test.go b/controller/metrics/metrics_test.go index 00309fb0944a7..61a99a46492a2 100644 --- a/controller/metrics/metrics_test.go +++ b/controller/metrics/metrics_test.go @@ -66,6 +66,10 @@ spec: source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git + syncPolicy: + automated: + selfHeal: false + prune: true status: sync: status: Synced @@ -97,6 +101,10 @@ spec: source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git + syncPolicy: + automated: + selfHeal: true + prune: false status: sync: status: OutOfSync @@ -227,9 +235,9 @@ func TestMetrics(t *testing.T) { responseContains: ` # HELP argocd_app_info Information about application. # TYPE argocd_app_info gauge -argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1 -argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 -argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 +argocd_app_info{autosync_enabled="true",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1 +argocd_app_info{autosync_enabled="false",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 +argocd_app_info{autosync_enabled="true",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 `, }, { @@ -237,7 +245,7 @@ argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost: responseContains: ` # HELP argocd_app_info Information about application. # TYPE argocd_app_info gauge -argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 +argocd_app_info{autosync_enabled="false",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1 `, }, } diff --git a/docs/operator-manual/application.yaml b/docs/operator-manual/application.yaml index 26baa8e9e1771..076e348f30801 100644 --- a/docs/operator-manual/application.yaml +++ b/docs/operator-manual/application.yaml @@ -188,6 +188,7 @@ spec: - CreateNamespace=true # Namespace Auto-Creation ensures that namespace specified as the application destination exists in the destination cluster. - PrunePropagationPolicy=foreground # Supported policies are background, foreground and orphan. - PruneLast=true # Allow the ability for resource pruning to happen as a final, implicit wave of a sync operation + - RespectIgnoreDifferences=true # When syncing changes, respect fields ignored by the ignoreDifferences configuration managedNamespaceMetadata: # Sets the metadata for the application namespace. Only valid if CreateNamespace=true (see above), otherwise it's a no-op. labels: # The labels to set on the application namespace any: label @@ -206,7 +207,7 @@ spec: maxDuration: 3m # the maximum amount of time allowed for the backoff strategy # Will ignore differences between live and desired states during the diff. Note that these configurations are not - # used during the sync process. + # used during the sync process unless the `RespectIgnoreDifferences=true` sync option is enabled. ignoreDifferences: # for the specified json pointers - group: apps @@ -218,6 +219,9 @@ spec: kind: "*" managedFieldsManagers: - kube-controller-manager + # Name and namespace are optional. If specified, they must match exactly, these are not glob patterns. + name: my-deployment + namespace: my-namespace # RevisionHistoryLimit limits the number of items kept in the application's revision history, which is used for # informational purposes as well as for rollbacks to previous versions. This should only be changed in exceptional diff --git a/docs/operator-manual/applicationset/Appset-Any-Namespace.md b/docs/operator-manual/applicationset/Appset-Any-Namespace.md index adf694f655f13..494b36dbdcf36 100644 --- a/docs/operator-manual/applicationset/Appset-Any-Namespace.md +++ b/docs/operator-manual/applicationset/Appset-Any-Namespace.md @@ -23,7 +23,48 @@ This feature needs [App in any namespace](../app-any-namespace.md) feature activ This feature can only be enabled and used when your Argo CD ApplicationSet controller is installed as a cluster-wide instance, so it has permissions to list and manipulate resources on a cluster scope. It will *not* work with an Argo CD installed in namespace-scoped mode. -## Implementation details +### SCM Providers secrets consideration + +By allowing ApplicationSet in any namespace you must be aware that any secrets can be exfiltrated using `scmProvider` or `pullRequest` generators. + +Here is an example: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: myapps +spec: + generators: + - scmProvider: + gitea: + # The Gitea owner to scan. + owner: myorg + # With this malicious setting, user can send all request to a Pod that will log incoming requests including headers with tokens + api: http://my-service.my-namespace.svc.cluster.local + # If true, scan every branch of every repository. If false, scan only the default branch. Defaults to false. + allBranches: true + # By changing this token reference, user can exfiltrate any secrets + tokenRef: + secretName: gitea-token + key: token + template: +``` + +Therefore administrator must restrict the urls of the allowed SCM Providers (example: `https://git.mydomain.com/,https://gitlab.mydomain.com/`) by setting the environment variable `ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS` to argocd-cmd-params-cm `applicationsetcontroller.allowed.scm.providers`. If another url is used, it will be rejected by the applicationset controller. + + +For example: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cmd-params-cm +data: + applicationsetcontroller.allowed.scm.providers: https://git.mydomain.com/,https://gitlab.mydomain.com/ +``` + +> Please note url used in the `api` field of the `ApplicationSet` must match the url declared by the Administrator including the protocol ### Overview @@ -163,9 +204,9 @@ For other operations such as `POST` and `PUT`, the `appNamespace` parameter must For `ApplicationSet` resources in the control plane namespace, this parameter can be omitted. -## Secrets consideration +## Clusters secrets consideration -By allowing ApplicationSet in any namespace you must be aware that clusters, API token secrets (etc...) can be discovered and used. +By allowing ApplicationSet in any namespace you must be aware that clusters can be discovered and used. Example: @@ -177,4 +218,4 @@ spec: - clusters: {} # Automatically use all clusters defined within Argo CD ``` -If you don't want to allow users to discover secrets with ApplicationSets from other namespaces you may consider deploying ArgoCD in namespace scope or use OPA rules. \ No newline at end of file +If you don't want to allow users to discover all clusters with ApplicationSets from other namespaces you may consider deploying ArgoCD in namespace scope or use OPA rules. \ No newline at end of file diff --git a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md index f43a9dbd359ba..b9c383cda404f 100644 --- a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md +++ b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md @@ -195,10 +195,6 @@ By default, the Argo CD notifications and the Argo CD refresh type annotations a Here is a list of commonly requested resource modification features which are not supported as of the current release. This lack of support is *not* necessarily by design; rather these behaviours are documented here to provide clear, concise descriptions of the current state of the feature. -### Limitation: Control resource modification on a per ApplicationSet basis - -There is currently no way to restrict modification/deletion of the Applications that are owned by an *individual* ApplicationSet. The global `--policy` parameters described above only allow targeting of *all* ApplicationSets (eg it is 'all or nothing'). - ### Limitation: No support for manual edits to individual Applications There is currently no way to allow modification of a single child Application of an ApplicationSet, for example, if you wanted to make manual edits to a single Application for debugging/testing purposes. diff --git a/docs/operator-manual/argocd-cmd-params-cm.yaml b/docs/operator-manual/argocd-cmd-params-cm.yaml index 7dae71629995a..d4a754f0e44b9 100644 --- a/docs/operator-manual/argocd-cmd-params-cm.yaml +++ b/docs/operator-manual/argocd-cmd-params-cm.yaml @@ -179,6 +179,11 @@ data: applicationsetcontroller.namespaces: "argocd,argocd-appsets-*" # Path of the self-signed TLS certificate for SCM/PR Gitlab Generator applicationsetcontroller.scm.root.ca.path: "" + # A comma separated list of allowed SCM providers (default "" is all SCM providers). + # Setting this field is required when using ApplicationSets-in-any-namespace, to prevent users from + # sending secrets from `tokenRef`s to disallowed `api` domains. + # The url used in the scm generator must exactly match one in the list + applicationsetcontroller.allowed.scm.providers: "https://git.example.com/,https://gitlab.example.com/" ## Argo CD Notifications Controller Properties # Set the logging level. One of: debug|info|warn|error (default "info") diff --git a/docs/operator-manual/config-management-plugins.md b/docs/operator-manual/config-management-plugins.md index ae39bf1a9214a..3550dbd81f143 100644 --- a/docs/operator-manual/config-management-plugins.md +++ b/docs/operator-manual/config-management-plugins.md @@ -106,7 +106,7 @@ spec: # static parameter announcements list. command: [echo, '[{"name": "example-param", "string": "default-string-value"}]'] - # If set to then the plugin receives repository files with original file mode. Dangerous since the repository + # If set to `true` then the plugin receives repository files with original file mode. Dangerous since the repository # might have executable files. Set to true only if you trust the CMP plugin authors. preserveFileMode: false ``` diff --git a/docs/operator-manual/deep_links.md b/docs/operator-manual/deep_links.md index df46cced2ae90..c166a1d25d75d 100644 --- a/docs/operator-manual/deep_links.md +++ b/docs/operator-manual/deep_links.md @@ -37,7 +37,7 @@ Each link in the list has five subfields: As mentioned earlier the links and conditions can be templated to use data from the resource, each category of links can access different types of data linked to that resource. Overall we have these 4 resources available for templating in the system: -- `application`: this key is used to access the application resource data. +- `app` or `application`: this key is used to access the application resource data. - `resource`: this key is used to access values for the actual k8s resource. - `cluster`: this key is used to access the related destination cluster data like name, server, namespaces etc. - `project`: this key is used to access the project resource data. @@ -45,7 +45,7 @@ Overall we have these 4 resources available for templating in the system: The above resources are accessible in particular link categories, here's a list of resources available in each category: - `resource.links`: `resource`, `application`, `cluster` and `project` -- `application.links`: `application` and `cluster` +- `application.links`: `app`/`application` and `cluster` - `project.links`: `project` An example `argocd-cm.yaml` file with deep links and their variations : @@ -60,16 +60,16 @@ An example `argocd-cm.yaml` file with deep links and their variations : # sample application level links application.links: | # pkg.go.dev/text/template is used for evaluating url templates - - url: https://mycompany.splunk.com?search={{.application.spec.destination.namespace}}&env={{.project.metadata.labels.env}} + - url: https://mycompany.splunk.com?search={{.app.spec.destination.namespace}}&env={{.project.metadata.labels.env}} title: Splunk # conditionally show link e.g. for specific project # github.com/antonmedv/expr is used for evaluation of conditions - - url: https://mycompany.splunk.com?search={{.application.spec.destination.namespace}} + - url: https://mycompany.splunk.com?search={{.app.spec.destination.namespace}} title: Splunk if: application.spec.project == "default" - - url: https://{{.application.metadata.annotations.splunkhost}}?search={{.application.spec.destination.namespace}} + - url: https://{{.app.metadata.annotations.splunkhost}}?search={{.app.spec.destination.namespace}} title: Splunk - if: application.metadata.annotations.splunkhost != "" + if: app.metadata.annotations.splunkhost != "" # sample resource level links resource.links: | - url: https://mycompany.splunk.com?search={{.resource.metadata.name}}&env={{.project.metadata.labels.env}} diff --git a/docs/snyk/index.md b/docs/snyk/index.md index b27c3646f1e17..4908a8bed515f 100644 --- a/docs/snyk/index.md +++ b/docs/snyk/index.md @@ -14,62 +14,62 @@ recent minor releases. | | Critical | High | Medium | Low | |---:|:--------:|:----:|:------:|:---:| | [go.mod](master/argocd-test.html) | 0 | 1 | 0 | 0 | -| [ui/yarn.lock](master/argocd-test.html) | 0 | 1 | 0 | 0 | -| [dex:v2.37.0](master/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 0 | 0 | -| [haproxy:2.6.14-alpine](master/haproxy_2.6.14-alpine.html) | 0 | 0 | 0 | 0 | -| [argocd:latest](master/quay.io_argoproj_argocd_latest.html) | 0 | 0 | 3 | 17 | -| [redis:7.0.11-alpine](master/redis_7.0.11-alpine.html) | 0 | 0 | 0 | 0 | +| [ui/yarn.lock](master/argocd-test.html) | 0 | 0 | 0 | 0 | +| [dex:v2.37.0](master/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 2 | 0 | +| [haproxy:2.6.14-alpine](master/haproxy_2.6.14-alpine.html) | 0 | 0 | 2 | 0 | +| [argocd:latest](master/quay.io_argoproj_argocd_latest.html) | 0 | 0 | 3 | 15 | +| [redis:7.0.11-alpine](master/redis_7.0.11-alpine.html) | 0 | 0 | 2 | 0 | | [install.yaml](master/argocd-iac-install.html) | - | - | - | - | | [namespace-install.yaml](master/argocd-iac-namespace-install.html) | - | - | - | - | -### v2.8.0-rc2 +### v2.8.0-rc6 | | Critical | High | Medium | Low | |---:|:--------:|:----:|:------:|:---:| -| [go.mod](v2.8.0-rc2/argocd-test.html) | 0 | 1 | 0 | 0 | -| [ui/yarn.lock](v2.8.0-rc2/argocd-test.html) | 0 | 1 | 0 | 0 | -| [dex:v2.37.0](v2.8.0-rc2/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 0 | 0 | -| [haproxy:2.6.14-alpine](v2.8.0-rc2/haproxy_2.6.14-alpine.html) | 0 | 0 | 0 | 0 | -| [argocd:v2.8.0-rc2](v2.8.0-rc2/quay.io_argoproj_argocd_v2.8.0-rc2.html) | 0 | 0 | 3 | 17 | -| [redis:7.0.11-alpine](v2.8.0-rc2/redis_7.0.11-alpine.html) | 0 | 0 | 0 | 0 | -| [install.yaml](v2.8.0-rc2/argocd-iac-install.html) | - | - | - | - | -| [namespace-install.yaml](v2.8.0-rc2/argocd-iac-namespace-install.html) | - | - | - | - | +| [go.mod](v2.8.0-rc6/argocd-test.html) | 0 | 1 | 0 | 0 | +| [ui/yarn.lock](v2.8.0-rc6/argocd-test.html) | 0 | 0 | 0 | 0 | +| [dex:v2.37.0](v2.8.0-rc6/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 2 | 0 | +| [haproxy:2.6.14-alpine](v2.8.0-rc6/haproxy_2.6.14-alpine.html) | 0 | 0 | 2 | 0 | +| [argocd:v2.8.0-rc6](v2.8.0-rc6/quay.io_argoproj_argocd_v2.8.0-rc6.html) | 0 | 0 | 3 | 15 | +| [redis:7.0.11-alpine](v2.8.0-rc6/redis_7.0.11-alpine.html) | 0 | 0 | 2 | 0 | +| [install.yaml](v2.8.0-rc6/argocd-iac-install.html) | - | - | - | - | +| [namespace-install.yaml](v2.8.0-rc6/argocd-iac-namespace-install.html) | - | - | - | - | -### v2.7.7 +### v2.7.9 | | Critical | High | Medium | Low | |---:|:--------:|:----:|:------:|:---:| -| [go.mod](v2.7.7/argocd-test.html) | 0 | 0 | 0 | 0 | -| [ui/yarn.lock](v2.7.7/argocd-test.html) | 0 | 1 | 0 | 0 | -| [dex:v2.37.0](v2.7.7/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 0 | 0 | -| [haproxy:2.6.14-alpine](v2.7.7/haproxy_2.6.14-alpine.html) | 0 | 0 | 0 | 0 | -| [argocd:v2.7.7](v2.7.7/quay.io_argoproj_argocd_v2.7.7.html) | 0 | 0 | 3 | 17 | -| [redis:7.0.11-alpine](v2.7.7/redis_7.0.11-alpine.html) | 0 | 0 | 0 | 0 | -| [install.yaml](v2.7.7/argocd-iac-install.html) | - | - | - | - | -| [namespace-install.yaml](v2.7.7/argocd-iac-namespace-install.html) | - | - | - | - | +| [go.mod](v2.7.9/argocd-test.html) | 0 | 0 | 0 | 0 | +| [ui/yarn.lock](v2.7.9/argocd-test.html) | 0 | 1 | 0 | 0 | +| [dex:v2.37.0](v2.7.9/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 2 | 0 | +| [haproxy:2.6.14-alpine](v2.7.9/haproxy_2.6.14-alpine.html) | 0 | 0 | 2 | 0 | +| [argocd:v2.7.9](v2.7.9/quay.io_argoproj_argocd_v2.7.9.html) | 0 | 0 | 4 | 15 | +| [redis:7.0.11-alpine](v2.7.9/redis_7.0.11-alpine.html) | 0 | 0 | 2 | 0 | +| [install.yaml](v2.7.9/argocd-iac-install.html) | - | - | - | - | +| [namespace-install.yaml](v2.7.9/argocd-iac-namespace-install.html) | - | - | - | - | -### v2.6.12 +### v2.6.13 | | Critical | High | Medium | Low | |---:|:--------:|:----:|:------:|:---:| -| [go.mod](v2.6.12/argocd-test.html) | 0 | 0 | 0 | 0 | -| [ui/yarn.lock](v2.6.12/argocd-test.html) | 0 | 1 | 0 | 0 | -| [dex:v2.37.0](v2.6.12/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 0 | 0 | -| [haproxy:2.6.14-alpine](v2.6.12/haproxy_2.6.14-alpine.html) | 0 | 0 | 0 | 0 | -| [argocd:v2.6.12](v2.6.12/quay.io_argoproj_argocd_v2.6.12.html) | 0 | 0 | 3 | 17 | -| [redis:7.0.11-alpine](v2.6.12/redis_7.0.11-alpine.html) | 0 | 0 | 0 | 0 | -| [install.yaml](v2.6.12/argocd-iac-install.html) | - | - | - | - | -| [namespace-install.yaml](v2.6.12/argocd-iac-namespace-install.html) | - | - | - | - | +| [go.mod](v2.6.13/argocd-test.html) | 0 | 0 | 0 | 0 | +| [ui/yarn.lock](v2.6.13/argocd-test.html) | 0 | 1 | 0 | 0 | +| [dex:v2.37.0](v2.6.13/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 2 | 0 | +| [haproxy:2.6.14-alpine](v2.6.13/haproxy_2.6.14-alpine.html) | 0 | 0 | 2 | 0 | +| [argocd:v2.6.13](v2.6.13/quay.io_argoproj_argocd_v2.6.13.html) | 0 | 0 | 4 | 15 | +| [redis:7.0.11-alpine](v2.6.13/redis_7.0.11-alpine.html) | 0 | 0 | 2 | 0 | +| [install.yaml](v2.6.13/argocd-iac-install.html) | - | - | - | - | +| [namespace-install.yaml](v2.6.13/argocd-iac-namespace-install.html) | - | - | - | - | -### v2.5.20 +### v2.5.21 | | Critical | High | Medium | Low | |---:|:--------:|:----:|:------:|:---:| -| [go.mod](v2.5.20/argocd-test.html) | 0 | 0 | 2 | 0 | -| [ui/yarn.lock](v2.5.20/argocd-test.html) | 0 | 1 | 4 | 0 | -| [dex:v2.37.0](v2.5.20/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 0 | 0 | -| [haproxy:2.6.14-alpine](v2.5.20/haproxy_2.6.14-alpine.html) | 0 | 0 | 0 | 0 | -| [argocd:v2.5.20](v2.5.20/quay.io_argoproj_argocd_v2.5.20.html) | 0 | 0 | 3 | 17 | -| [redis:7.0.11-alpine](v2.5.20/redis_7.0.11-alpine.html) | 0 | 0 | 0 | 0 | -| [install.yaml](v2.5.20/argocd-iac-install.html) | - | - | - | - | -| [namespace-install.yaml](v2.5.20/argocd-iac-namespace-install.html) | - | - | - | - | +| [go.mod](v2.5.21/argocd-test.html) | 0 | 0 | 2 | 0 | +| [ui/yarn.lock](v2.5.21/argocd-test.html) | 0 | 1 | 4 | 0 | +| [dex:v2.37.0](v2.5.21/ghcr.io_dexidp_dex_v2.37.0.html) | 0 | 0 | 2 | 0 | +| [haproxy:2.6.14-alpine](v2.5.21/haproxy_2.6.14-alpine.html) | 0 | 0 | 2 | 0 | +| [argocd:v2.5.21](v2.5.21/quay.io_argoproj_argocd_v2.5.21.html) | 0 | 0 | 4 | 15 | +| [redis:7.0.11-alpine](v2.5.21/redis_7.0.11-alpine.html) | 0 | 0 | 2 | 0 | +| [install.yaml](v2.5.21/argocd-iac-install.html) | - | - | - | - | +| [namespace-install.yaml](v2.5.21/argocd-iac-namespace-install.html) | - | - | - | - | diff --git a/docs/snyk/master/argocd-iac-install.html b/docs/snyk/master/argocd-iac-install.html index 7b11441beb9e2..dfe207c217a92 100644 --- a/docs/snyk/master/argocd-iac-install.html +++ b/docs/snyk/master/argocd-iac-install.html @@ -456,7 +456,7 @@
semver is a semantic version parser used by npm.
-Affected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS) via the function new Range
, when untrusted user data is provided as a range.
- const semver = require('semver')
- const lengths_2 = [2000, 4000, 8000, 16000, 32000, 64000, 128000]
-
- console.log("n[+] Valid range - Test payloads")
- for (let i = 0; i =1.2.3' + ' '.repeat(lengths_2[i]) + '<1.3.0';
- const start = Date.now()
- semver.validRange(value)
- // semver.minVersion(value)
- // semver.maxSatisfying(["1.2.3"], value)
- // semver.minSatisfying(["1.2.3"], value)
- // new semver.Range(value, {})
-
- const end = Date.now();
- console.log('length=%d, time=%d ms', value.length, end - start);
- }
-
- Denial of Service (DoS) describes a family of attacks, all aimed at making a system inaccessible to its original and legitimate users. There are many types of DoS attacks, ranging from trying to clog the network pipes to the system by generating a large volume of traffic from many machines (a Distributed Denial of Service - DDoS - attack) to sending crafted requests that cause a system to crash or take a disproportional amount of time to process.
-The Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren't very intuitive and can ultimately end up making it easy for attackers to take your site down.
-Let’s take the following regular expression as an example:
-regex = /A(B|C+)+D/
-
- This regular expression accomplishes the following:
-A
The string must start with the letter 'A'(B|C+)+
The string must then follow the letter A with either the letter 'B' or some number of occurrences of the letter 'C' (the +
matches one or more times). The +
at the end of this section states that we can look for one or more matches of this section.D
Finally, we ensure this section of the string ends with a 'D'The expression would match inputs such as ABBD
, ABCCCCD
, ABCBCCCD
and ACCCCCD
It most cases, it doesn't take very long for a regex engine to find a match:
-$ time node -e '/A(B|C+)+D/.test("ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD")'
- 0.04s user 0.01s system 95% cpu 0.052 total
-
- $ time node -e '/A(B|C+)+D/.test("ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX")'
- 1.79s user 0.02s system 99% cpu 1.812 total
-
- The entire process of testing it against a 30 characters long string takes around ~52ms. But when given an invalid string, it takes nearly two seconds to complete the test, over ten times as long as it took to test a valid string. The dramatic difference is due to the way regular expressions get evaluated.
-Most Regex engines will work very similarly (with minor differences). The engine will match the first possible way to accept the current character and proceed to the next one. If it then fails to match the next one, it will backtrack and see if there was another way to digest the previous character. If it goes too far down the rabbit hole only to find out the string doesn’t match in the end, and if many characters have multiple valid regex paths, the number of backtracking steps can become very large, resulting in what is known as catastrophic backtracking.
-Let's look at how our expression runs into this problem, using a shorter string: "ACCCX". While it seems fairly straightforward, there are still four different ways that the engine could match those three C's:
-The engine has to try each of those combinations to see if any of them potentially match against the expression. When you combine that with the other steps the engine must take, we can use RegEx 101 debugger to see the engine has to take a total of 38 steps before it can determine the string doesn't match.
-From there, the number of steps the engine must use to validate a string just continues to grow.
-String | -Number of C's | -Number of steps | -
---|---|---|
ACCCX | -3 | -38 | -
ACCCCX | -4 | -71 | -
ACCCCCX | -5 | -136 | -
ACCCCCCCCCCCCCCX | -14 | -65,553 | -
By the time the string includes 14 C's, the engine has to take over 65,000 steps just to see if the string is valid. These extreme situations can cause them to work very slowly (exponentially related to input size, as shown above), allowing an attacker to exploit this and can cause the service to excessively consume CPU, resulting in a Denial of Service.
-Upgrade semver
to version 7.5.2 or higher.
Note: Versions mentioned in the description apply only to the upstream openssl
package and not the openssl
package as distributed by Alpine:3.18
.
+ See How to fix?
for Alpine:3.18
relevant fixed versions and status.
Issue summary: The AES-SIV cipher implementation contains a bug that causes + it to ignore empty associated data entries which are unauthenticated as + a consequence.
+Impact summary: Applications that use the AES-SIV algorithm and want to + authenticate empty data entries as associated data can be mislead by removing + adding or reordering such empty entries as these are ignored by the OpenSSL + implementation. We are currently unaware of any such applications.
+The AES-SIV algorithm allows for authentication of multiple associated + data entries along with the encryption. To authenticate empty data the + application has to call EVP_EncryptUpdate() (or EVP_CipherUpdate()) with + NULL pointer as the output buffer and 0 as the input buffer length. + The AES-SIV implementation in OpenSSL just returns success for such a call + instead of performing the associated data authentication operation. + The empty data thus will not be authenticated.
+As this issue does not affect non-empty associated data authentication and + we expect it to be rare for an application to use empty associated data + entries this is qualified as Low severity issue.
+Upgrade Alpine:3.18
openssl
to version 3.1.1-r2 or higher.
Note: Versions mentioned in the description apply only to the upstream openssl
package and not the openssl
package as distributed by Alpine:3.18
.
+ See How to fix?
for Alpine:3.18
relevant fixed versions and status.
Issue summary: Checking excessively long DH keys or parameters may be very slow.
+Impact summary: Applications that use the functions DH_check(), DH_check_ex() + or EVP_PKEY_param_check() to check a DH key or DH parameters may experience long + delays. Where the key or parameters that are being checked have been obtained + from an untrusted source this may lead to a Denial of Service.
+The function DH_check() performs various checks on DH parameters. One of those + checks confirms that the modulus ('p' parameter) is not too large. Trying to use + a very large modulus is slow and OpenSSL will not normally use a modulus which + is over 10,000 bits in length.
+However the DH_check() function checks numerous aspects of the key or parameters + that have been supplied. Some of those checks use the supplied modulus value + even if it has already been found to be too large.
+An application that calls DH_check() and supplies a key or parameters obtained + from an untrusted source could be vulernable to a Denial of Service attack.
+The function DH_check() is itself called by a number of other OpenSSL functions. + An application calling any of those other functions may similarly be affected. + The other functions affected by this are DH_check_ex() and + EVP_PKEY_param_check().
+Also vulnerable are the OpenSSL dhparam and pkeyparam command line applications + when using the '-check' option.
+The OpenSSL SSL/TLS implementation is not affected by this issue. + The OpenSSL 3.0 and 3.1 FIPS providers are not affected by this issue.
+Upgrade Alpine:3.18
openssl
to version 3.1.1-r3 or higher.