diff --git a/api/v1beta1/argocd_types.go b/api/v1beta1/argocd_types.go index 5653ad7dc..a88dbaed0 100644 --- a/api/v1beta1/argocd_types.go +++ b/api/v1beta1/argocd_types.go @@ -515,6 +515,9 @@ type ArgoCDRedisSpec struct { // Remote specifies the remote URL of the Redis container. (optional, by default, a local instance managed by the operator is used.) Remote *string `json:"remote,omitempty"` + + // TlsConfig defines the TLS configuration for the Redis server + TlsConfig *ArgoCDTlsConfig `json:"tlsConfig,omitempty"` } func (a *ArgoCDRedisSpec) IsEnabled() bool { @@ -600,6 +603,18 @@ type ArgoCDRepoSpec struct { // Custom certificates to inject into the repo server container and its plugins to trust source hosting sites SystemCATrust *ArgoCDSystemCATrustSpec `json:"systemCATrust,omitempty"` + // TLS configuration for the repo server + TlsConfig *ArgoCDTlsConfig `json:"tlsConfig,omitempty"` +} + +type ArgoCDTlsConfig struct { + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum="1.1";"1.2";"1.3";"tls1.1";"tls1.2";"tls1.3";"TLSv1.1";"TLSv1.2";"TLSv1.3" + MinVersion string `json:"minVersion,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum="1.1";"1.2";"1.3";"TLSv1.1";"TLSv1.2";"TLSv1.3";"tls1.1";"tls1.2";"tls1.3" + MaxVersion string `json:"maxVersion,omitempty"` + CipherSuites []string `json:"cipherSuites,omitempty"` } func (a *ArgoCDRepoSpec) IsEnabled() bool { @@ -734,6 +749,8 @@ type ArgoCDServerSpec struct { // Custom labels to pods deployed by the operator Labels map[string]string `json:"labels,omitempty"` + // TLS configuration for the Argo CD Server component + TlsConfig *ArgoCDTlsConfig `json:"tlsConfig,omitempty"` } func (a *ArgoCDServerSpec) IsEnabled() bool { @@ -1367,6 +1384,8 @@ type PrincipalTLSSpec struct { // InsecureGenerate is the flag to allow the principal to generate its own set of TLS cert and key on startup when none are configured InsecureGenerate *bool `json:"insecureGenerate,omitempty"` + // TLS configuration for the Principal component. + TlsConfig *ArgoCDTlsConfig `json:"tlsConfig,omitempty"` } // ArgoCDAgentPrincipalServiceSpec defines the options for the Service backing the ArgoCD Agent Principalcomponent. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b50eaf4a3..5fcdc7987 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -867,6 +867,11 @@ func (in *ArgoCDRedisSpec) DeepCopyInto(out *ArgoCDRedisSpec) { *out = new(string) **out = **in } + if in.TlsConfig != nil { + in, out := &in.TlsConfig, &out.TlsConfig + *out = new(ArgoCDTlsConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDRedisSpec. @@ -966,6 +971,11 @@ func (in *ArgoCDRepoSpec) DeepCopyInto(out *ArgoCDRepoSpec) { *out = new(ArgoCDSystemCATrustSpec) (*in).DeepCopyInto(*out) } + if in.TlsConfig != nil { + in, out := &in.TlsConfig, &out.TlsConfig + *out = new(ArgoCDTlsConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDRepoSpec. @@ -1170,6 +1180,11 @@ func (in *ArgoCDServerSpec) DeepCopyInto(out *ArgoCDServerSpec) { (*out)[key] = val } } + if in.TlsConfig != nil { + in, out := &in.TlsConfig, &out.TlsConfig + *out = new(ArgoCDTlsConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDServerSpec. @@ -1373,6 +1388,26 @@ func (in *ArgoCDTLSSpec) DeepCopy() *ArgoCDTLSSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgoCDTlsConfig) DeepCopyInto(out *ArgoCDTlsConfig) { + *out = *in + if in.CipherSuites != nil { + in, out := &in.CipherSuites, &out.CipherSuites + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDTlsConfig. +func (in *ArgoCDTlsConfig) DeepCopy() *ArgoCDTlsConfig { + if in == nil { + return nil + } + out := new(ArgoCDTlsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Banner) DeepCopyInto(out *Banner) { *out = *in @@ -1771,6 +1806,11 @@ func (in *PrincipalTLSSpec) DeepCopyInto(out *PrincipalTLSSpec) { *out = new(bool) **out = **in } + if in.TlsConfig != nil { + in, out := &in.TlsConfig, &out.TlsConfig + *out = new(ArgoCDTlsConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrincipalTLSSpec. diff --git a/bundle/manifests/argocd-operator.clusterserviceversion.yaml b/bundle/manifests/argocd-operator.clusterserviceversion.yaml index 5ea35331e..0e9007a4a 100644 --- a/bundle/manifests/argocd-operator.clusterserviceversion.yaml +++ b/bundle/manifests/argocd-operator.clusterserviceversion.yaml @@ -257,7 +257,7 @@ metadata: capabilities: Deep Insights categories: Integration & Delivery certified: "false" - createdAt: "2026-04-10T16:59:38Z" + createdAt: "2026-04-13T18:36:00Z" description: Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. operators.operatorframework.io/builder: operator-sdk-v1.35.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml index 43a1a1ec8..61f3b95ea 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -11920,6 +11920,38 @@ spec: description: SecretName is The name of the secret containing the TLS certificate and key. type: string + tlsConfig: + description: TLS configuration for the Principal component. + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object type: object type: object type: object @@ -18662,6 +18694,39 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tlsConfig: + description: TlsConfig defines the TLS configuration for the Redis + server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object version: description: Version is the Redis container image tag. type: string @@ -22246,6 +22311,38 @@ spec: x-kubernetes-map-type: atomic type: array type: object + tlsConfig: + description: TLS configuration for the repo server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object verifytls: description: VerifyTLS defines whether repo server API should be accessed using strict TLS validation @@ -27926,6 +28023,38 @@ spec: - name type: object type: array + tlsConfig: + description: TLS configuration for the Argo CD Server component + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object volumeMounts: description: VolumeMounts adds volumeMounts to the Argo CD Server container. diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 898bdfa0b..034a14a28 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -11909,6 +11909,38 @@ spec: description: SecretName is The name of the secret containing the TLS certificate and key. type: string + tlsConfig: + description: TLS configuration for the Principal component. + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object type: object type: object type: object @@ -18651,6 +18683,39 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tlsConfig: + description: TlsConfig defines the TLS configuration for the Redis + server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object version: description: Version is the Redis container image tag. type: string @@ -22235,6 +22300,38 @@ spec: x-kubernetes-map-type: atomic type: array type: object + tlsConfig: + description: TLS configuration for the repo server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object verifytls: description: VerifyTLS defines whether repo server API should be accessed using strict TLS validation @@ -27915,6 +28012,38 @@ spec: - name type: object type: array + tlsConfig: + description: TLS configuration for the Argo CD Server component + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object volumeMounts: description: VolumeMounts adds volumeMounts to the Argo CD Server container. diff --git a/controllers/argocd/deployment.go b/controllers/argocd/deployment.go index d4b1dde40..21565990e 100644 --- a/controllers/argocd/deployment.go +++ b/controllers/argocd/deployment.go @@ -220,14 +220,19 @@ func getArgoImportVolumes(cr *argoprojv1alpha1.ArgoCDExport) []corev1.Volume { return volumes } -func getArgoRedisArgs(useTLS bool) []string { +func getArgoRedisArgs(useTLS bool, cr *argoproj.ArgoCD) ([]string, error) { args := make([]string, 0) - args = append(args, "--save", "") args = append(args, "--appendonly", "no") args = append(args, "--aclfile", argoutil.RedisAuthMountPath+"users.acl") if useTLS { + arguments, err := argoutil.BuildRedisArgs(cr.Spec.Redis.TlsConfig) + if err != nil { + log.Error(err, "failed to build Redis args") + return nil, err + } + args = append(args, arguments...) args = append(args, "--tls-port", "6379") args = append(args, "--port", "0") @@ -235,8 +240,7 @@ func getArgoRedisArgs(useTLS bool) []string { args = append(args, "--tls-key-file", "/app/config/redis/tls/tls.key") args = append(args, "--tls-auth-clients", "no") } - - return args + return args, nil } // getArgoCmpServerInitCommand will return the command for the ArgoCD CMP Server init container @@ -440,9 +444,13 @@ func (r *ReconcileArgoCD) reconcileRedisDeployment(cr *argoproj.ArgoCD, useTLS b RunAsUser: int64Ptr(1000), } } - + arguments, err := getArgoRedisArgs(useTLS, cr) + if err != nil { + log.Error(err, "failed to get Redis args") + return err + } deploy.Spec.Template.Spec.Containers = []corev1.Container{{ - Args: getArgoRedisArgs(useTLS), + Args: arguments, Image: argoutil.GetRedisContainerImage(cr), ImagePullPolicy: argoutil.GetImagePullPolicy(cr.Spec.ImagePullPolicy), Name: "redis", @@ -896,7 +904,12 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoproj.ArgoCD, useTLSF serverVolumeMounts = append(serverVolumeMounts, cr.Spec.Server.VolumeMounts...) } + arguments, err := argoutil.BuildTLSArgs(cr.Spec.Server.TlsConfig) + if err != nil { + return err + } deploy.Spec.Template.Spec.Containers = []corev1.Container{{ + Args: arguments, Command: getArgoServerCommand(cr, useTLSForRedis), Image: getArgoContainerImage(cr), ImagePullPolicy: argoutil.GetImagePullPolicy(cr.Spec.ImagePullPolicy), @@ -1076,6 +1089,7 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoproj.ArgoCD, useTLSF argoutil.LogResourceDeletion(log, existing, "argocd server is disabled") return r.Delete(context.TODO(), existing) } + actualImage := existing.Spec.Template.Spec.Containers[0].Image desiredImage := getArgoContainerImage(cr) actualImagePullPolicy := existing.Spec.Template.Spec.Containers[0].ImagePullPolicy @@ -1086,6 +1100,10 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoproj.ArgoCD, useTLSF existing.Spec.Template.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST") changes = append(changes, "container image") } + if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers[0].Args, deploy.Spec.Template.Spec.Containers[0].Args) { + existing.Spec.Template.Spec.Containers[0].Args = deploy.Spec.Template.Spec.Containers[0].Args + changes = append(changes, "container args") + } if actualImagePullPolicy != desiredImagePullPolicy { existing.Spec.Template.Spec.Containers[0].ImagePullPolicy = desiredImagePullPolicy changes = append(changes, "image pull policy") diff --git a/controllers/argocd/deployment_test.go b/controllers/argocd/deployment_test.go index 8062207c4..670ee74a4 100644 --- a/controllers/argocd/deployment_test.go +++ b/controllers/argocd/deployment_test.go @@ -1357,6 +1357,7 @@ func TestReconcileArgoCD_reconcileServerDeployment(t *testing.T) { Name: "argocd-server", Image: getArgoContainerImage(a), ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{"--tlsminversion", "1.3", "--tlsmaxversion", "1.3"}, Command: []string{ "argocd-server", "--staticassets", @@ -1847,6 +1848,7 @@ func TestReconcileArgoCD_reconcileServerDeploymentWithInsecure(t *testing.T) { Name: "argocd-server", Image: getArgoContainerImage(a), ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{"--tlsminversion", "1.3", "--tlsmaxversion", "1.3"}, Command: []string{ "argocd-server", "--insecure", @@ -1932,6 +1934,7 @@ func TestReconcileArgoCD_reconcileServerDeploymentChangedToInsecure(t *testing.T Name: "argocd-server", Image: getArgoContainerImage(a), ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{"--tlsminversion", "1.3", "--tlsmaxversion", "1.3"}, Command: []string{ "argocd-server", "--insecure", @@ -2025,6 +2028,7 @@ func TestReconcileArgoCD_reconcileRedisDeploymentWithTLS(t *testing.T) { "--save", "", "--appendonly", "no", "--aclfile", "/app/config/redis-auth/users.acl", + "--tls-protocols", "TLSv1.3", "--tls-port", "6379", "--port", "0", "--tls-cert-file", "/app/config/redis/tls/tls.crt", diff --git a/controllers/argocd/repo_server.go b/controllers/argocd/repo_server.go index 1d7517a86..fe2ced5f3 100644 --- a/controllers/argocd/repo_server.go +++ b/controllers/argocd/repo_server.go @@ -214,7 +214,12 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argocdoperatorv1beta1.Argo repoServerVolumeMounts = append(repoServerVolumeMounts, cr.Spec.Repo.VolumeMounts...) } + arguments, error := argoutil.BuildTLSArgs(cr.Spec.Repo.TlsConfig) + if error != nil { + return error + } deploy.Spec.Template.Spec.Containers = []corev1.Container{{ + Args: arguments, Command: getArgoRepoCommand(cr, useTLSForRedis), Image: getRepoServerContainerImage(cr), ImagePullPolicy: argoutil.GetImagePullPolicy(cr.Spec.ImagePullPolicy), @@ -400,6 +405,10 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argocdoperatorv1beta1.Argo desiredImage := getRepoServerContainerImage(cr) actualImagePullPolicy := existing.Spec.Template.Spec.Containers[0].ImagePullPolicy desiredImagePullPolicy := argoutil.GetImagePullPolicy(cr.Spec.ImagePullPolicy) + if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers[0].Args, deploy.Spec.Template.Spec.Containers[0].Args) { + existing.Spec.Template.Spec.Containers[0].Args = deploy.Spec.Template.Spec.Containers[0].Args + changes = append(changes, "container args") + } if actualImage != desiredImage { existing.Spec.Template.Spec.Containers[0].Image = desiredImage if existing.Spec.Template.Labels == nil { diff --git a/controllers/argocdagent/deployment.go b/controllers/argocdagent/deployment.go index 0eb631caa..e7fa0652e 100644 --- a/controllers/argocdagent/deployment.go +++ b/controllers/argocdagent/deployment.go @@ -40,6 +40,7 @@ import ( // It creates, updates, or deletes the deployment based on the ArgoCD CR configuration. func ReconcilePrincipalDeployment(client client.Client, compName, saName string, cr *argoproj.ArgoCD, scheme *runtime.Scheme) error { deployment := buildDeployment(compName, cr) + var err error // Check if deployment already exists exists := true @@ -60,7 +61,10 @@ func ReconcilePrincipalDeployment(client client.Client, compName, saName string, return nil } - deployment, changed := updateDeploymentIfChanged(compName, saName, cr, deployment) + deployment, changed, err := updateDeploymentIfChanged(compName, saName, cr, deployment) + if err != nil { + return err + } if changed { argoutil.LogResourceUpdate(log, deployment, "principal deployment is being updated") if err := client.Update(context.TODO(), deployment); err != nil { @@ -80,7 +84,10 @@ func ReconcilePrincipalDeployment(client client.Client, compName, saName string, } argoutil.LogResourceCreation(log, deployment) - deployment.Spec = buildPrincipalSpec(compName, saName, cr) + deployment.Spec, err = buildPrincipalSpec(compName, saName, cr) + if err != nil { + return err + } if err := client.Create(context.TODO(), deployment); err != nil { return fmt.Errorf("failed to create principal deployment %s in namespace %s: %v", deployment.Name, cr.Namespace, err) } @@ -97,8 +104,13 @@ func buildDeployment(compName string, cr *argoproj.ArgoCD) *appsv1.Deployment { } } -func buildPrincipalSpec(compName, saName string, cr *argoproj.ArgoCD) appsv1.DeploymentSpec { +func buildPrincipalSpec(compName, saName string, cr *argoproj.ArgoCD) (appsv1.DeploymentSpec, error) { redisAuthVolume, redisAuthMount := argoutil.MountRedisAuthToArgo(cr) + envParams, err := buildPrincipalContainerEnv(cr) + if err != nil { + log.Error(err, "failed to build principal container env") + return appsv1.DeploymentSpec{}, err + } return appsv1.DeploymentSpec{ Selector: buildSelector(compName, cr), Template: corev1.PodTemplateSpec{ @@ -111,7 +123,7 @@ func buildPrincipalSpec(compName, saName string, cr *argoproj.ArgoCD) appsv1.Dep Image: buildPrincipalImage(cr), ImagePullPolicy: argoutil.GetImagePullPolicy(cr.Spec.ImagePullPolicy), Name: generateAgentResourceName(cr.Name, compName), - Env: buildPrincipalContainerEnv(cr), + Env: envParams, Args: buildArgs(compName), SecurityContext: buildSecurityContext(), Ports: buildPorts(compName), @@ -122,7 +134,7 @@ func buildPrincipalSpec(compName, saName string, cr *argoproj.ArgoCD) appsv1.Dep Volumes: append(buildVolumes(), redisAuthVolume), }, }, - } + }, nil } func buildSelector(compName string, cr *argoproj.ArgoCD) *metav1.LabelSelector { @@ -249,7 +261,7 @@ func buildVolumes() []corev1.Volume { // updateDeploymentIfChanged compares the current deployment with the desired state // and updates it if any changes are detected. Returns the updated deployment and a boolean // indicating whether any changes were made. -func updateDeploymentIfChanged(compName, saName string, cr *argoproj.ArgoCD, deployment *appsv1.Deployment) (*appsv1.Deployment, bool) { +func updateDeploymentIfChanged(compName, saName string, cr *argoproj.ArgoCD, deployment *appsv1.Deployment) (*appsv1.Deployment, bool, error) { changed := false if !reflect.DeepEqual(deployment.Spec.Selector, buildSelector(compName, cr)) { @@ -275,11 +287,15 @@ func updateDeploymentIfChanged(compName, saName string, cr *argoproj.ArgoCD, dep changed = true deployment.Spec.Template.Spec.Containers[0].Name = generateAgentResourceName(cr.Name, compName) } - - if !reflect.DeepEqual(deployment.Spec.Template.Spec.Containers[0].Env, buildPrincipalContainerEnv(cr)) { + envParams, err := buildPrincipalContainerEnv(cr) + if err != nil { + log.Error(err, "failed to build principal container env") + return nil, changed, err + } + if !reflect.DeepEqual(deployment.Spec.Template.Spec.Containers[0].Env, envParams) { log.Info("deployment container env is being updated") changed = true - deployment.Spec.Template.Spec.Containers[0].Env = buildPrincipalContainerEnv(cr) + deployment.Spec.Template.Spec.Containers[0].Env = envParams } if !reflect.DeepEqual(deployment.Spec.Template.Spec.Containers[0].Args, buildArgs(compName)) { @@ -306,10 +322,15 @@ func updateDeploymentIfChanged(compName, saName string, cr *argoproj.ArgoCD, dep deployment.Spec.Template.Spec.ServiceAccountName = saName } - return deployment, changed + return deployment, changed, nil } -func buildPrincipalContainerEnv(cr *argoproj.ArgoCD) []corev1.EnvVar { +func buildPrincipalContainerEnv(cr *argoproj.ArgoCD) ([]corev1.EnvVar, error) { + arguments, err := getPrincipalTlsConfig(cr) + if err != nil { + log.Error(err, "failed to get principal TLS config") + return nil, err + } env := []corev1.EnvVar{ { Name: EnvArgoCDPrincipalLogLevel, @@ -368,6 +389,15 @@ func buildPrincipalContainerEnv(cr *argoproj.ArgoCD) []corev1.EnvVar { }, { Name: EnvArgoCDPrincipalResourceProxyCaSecretName, Value: getPrincipalResourceProxyCaSecretName(cr), + }, { + Name: EnvArgoCDPrincipalTlsMinVersion, + Value: arguments["--tlsminversion"], + }, { + Name: EnvArgoCDPrincipalTlsMaxVersion, + Value: arguments["--tlsmaxversion"], + }, { + Name: EnvArgoCDPrincipalCipherSuites, + Value: arguments["--tlsciphers"], }, { Name: EnvArgoCDPrincipalJwtSecretName, Value: getPrincipalJWTSecretName(cr), @@ -384,7 +414,7 @@ func buildPrincipalContainerEnv(cr *argoproj.ArgoCD) []corev1.EnvVar { env = append(env, cr.Spec.ArgoCDAgent.Principal.Env...) } - return env + return env, nil } // These constants are environment variables that correspond to the environment variables @@ -412,6 +442,9 @@ const ( EnvArgoCDPrincipalJwtSecretName = "ARGOCD_PRINCIPAL_JWT_SECRET_NAME" EnvArgoCDPrincipalImage = "ARGOCD_PRINCIPAL_IMAGE" EnvArgoCDPrincipalDestinationBasedMapping = "ARGOCD_PRINCIPAL_DESTINATION_BASED_MAPPING" + EnvArgoCDPrincipalTlsMinVersion = "ARGOCD_PRINCIPAL_TLS_MIN_VERSION" + EnvArgoCDPrincipalTlsMaxVersion = "ARGOCD_PRINCIPAL_TLS_MAX_VERSION" + EnvArgoCDPrincipalCipherSuites = "ARGOCD_PRINCIPAL_TLS_CIPHERSUITES" ) // Logging Configuration @@ -534,6 +567,21 @@ func getPrincipalResourceProxyCaSecretName(cr *argoproj.ArgoCD) string { return "argocd-agent-ca" } +func getPrincipalTlsConfig(cr *argoproj.ArgoCD) (map[string]string, error) { + arguments := make(map[string]string) + if hasTLS(cr) { + arguments, err := argoutil.BuildArgoCDAgentTLSArgs(cr.Spec.ArgoCDAgent.Principal.TLS.TlsConfig, arguments) + if err != nil { + return nil, err + } + return arguments, nil + } + arguments["--tlsminversion"] = "tls1.3" + arguments["--tlsmaxversion"] = "tls1.3" + arguments["--tlsciphers"] = "" + return arguments, nil +} + func getPrincipalResourceProxySecretName(cr *argoproj.ArgoCD) string { if hasResourceProxy(cr) && cr.Spec.ArgoCDAgent.Principal.ResourceProxy.SecretName != "" { return cr.Spec.ArgoCDAgent.Principal.ResourceProxy.SecretName diff --git a/controllers/argocdagent/deployment_test.go b/controllers/argocdagent/deployment_test.go index 9357e3260..d66cf4c03 100644 --- a/controllers/argocdagent/deployment_test.go +++ b/controllers/argocdagent/deployment_test.go @@ -32,22 +32,29 @@ import ( ) // Helper function to create a test deployment -func makeTestDeployment(cr *argoproj.ArgoCD) *appsv1.Deployment { +func makeTestDeployment(cr *argoproj.ArgoCD) (*appsv1.Deployment, error) { + spec, err := buildPrincipalSpec(testCompName, generateAgentResourceName(cr.Name, testCompName), cr) + if err != nil { + return nil, err + } return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: generateAgentResourceName(cr.Name, testCompName), Namespace: cr.Namespace, Labels: buildLabelsForAgentPrincipal(cr.Name, testCompName), }, - Spec: buildPrincipalSpec(testCompName, generateAgentResourceName(cr.Name, testCompName), cr), - } + Spec: spec, + }, nil } // Helper function to create a test deployment with custom image -func makeTestDeploymentWithCustomImage(cr *argoproj.ArgoCD, customImage string) *appsv1.Deployment { - deployment := makeTestDeployment(cr) +func makeTestDeploymentWithCustomImage(cr *argoproj.ArgoCD, customImage string) (*appsv1.Deployment, error) { + deployment, err := makeTestDeployment(cr) + if err != nil { + return nil, err + } deployment.Spec.Template.Spec.Containers[0].Image = customImage - return deployment + return deployment, nil } // Helper function to create ArgoCD with custom principal image @@ -116,18 +123,21 @@ func TestReconcilePrincipalDeployment_DeploymentDoesNotExist_PrincipalEnabled(t assert.Equal(t, buildLabelsForAgentPrincipal(cr.Name, testCompName), deployment.Labels) // Verify Deployment has expected spec - expectedSpec := buildPrincipalSpec(testCompName, saName, cr) + expectedSpec, err := buildPrincipalSpec(testCompName, saName, cr) + assert.NoError(t, err) assert.Equal(t, expectedSpec.Selector, deployment.Spec.Selector) assert.Equal(t, expectedSpec.Template.Labels, deployment.Spec.Template.Labels) assert.Equal(t, expectedSpec.Template.Spec.ServiceAccountName, deployment.Spec.Template.Spec.ServiceAccountName) // Verify container configuration + envParams, err := buildPrincipalContainerEnv(cr) + assert.NoError(t, err) assert.Len(t, deployment.Spec.Template.Spec.Containers, 1) container := deployment.Spec.Template.Spec.Containers[0] assert.Equal(t, generateAgentResourceName(cr.Name, testCompName), container.Name) assert.Equal(t, buildPrincipalImage(cr), container.Image) assert.Equal(t, buildArgs(testCompName), container.Args) - assert.Equal(t, buildPrincipalContainerEnv(cr), container.Env) + assert.Equal(t, envParams, container.Env) assert.Equal(t, buildSecurityContext(), container.SecurityContext) assert.Equal(t, buildPorts(testCompName), container.Ports) @@ -145,13 +155,14 @@ func TestReconcilePrincipalDeployment_DeploymentExists_PrincipalDisabled(t *test saName := generateAgentResourceName(cr.Name, testCompName) // Create existing Deployment - existingDeployment := makeTestDeployment(cr) + existingDeployment, err := makeTestDeployment(cr) + assert.NoError(t, err) resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) assert.NoError(t, err) // Verify Deployment was deleted @@ -171,13 +182,14 @@ func TestReconcilePrincipalDeployment_DeploymentExists_PrincipalEnabled_NoChange saName := generateAgentResourceName(cr.Name, testCompName) // Create existing Deployment with correct spec - existingDeployment := makeTestDeployment(cr) + existingDeployment, err := makeTestDeployment(cr) + assert.NoError(t, err) resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) assert.NoError(t, err) // Verify Deployment still exists with same spec @@ -199,13 +211,14 @@ func TestReconcilePrincipalDeployment_DeploymentExists_PrincipalEnabled_ImageCha saName := generateAgentResourceName(cr.Name, testCompName) // Create existing Deployment with old image - existingDeployment := makeTestDeploymentWithCustomImage(cr, "quay.io/argoproj/argocd-agent:v1") + existingDeployment, err := makeTestDeploymentWithCustomImage(cr, "quay.io/argoproj/argocd-agent:v1") + assert.NoError(t, err) resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) assert.NoError(t, err) // Verify Deployment was updated with new image @@ -227,14 +240,15 @@ func TestReconcilePrincipalDeployment_DeploymentExists_PrincipalEnabled_ServiceA newSAName := "new-service-account" // Create existing Deployment with old service account - existingDeployment := makeTestDeployment(cr) + existingDeployment, err := makeTestDeployment(cr) + assert.NoError(t, err) existingDeployment.Spec.Template.Spec.ServiceAccountName = oldSAName resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, newSAName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, newSAName, cr, sch) assert.NoError(t, err) // Verify Deployment was updated with new service account @@ -255,13 +269,14 @@ func TestReconcilePrincipalDeployment_DeploymentExists_PrincipalNotSet(t *testin saName := generateAgentResourceName(cr.Name, testCompName) // Create existing Deployment - existingDeployment := makeTestDeployment(cr) + existingDeployment, err := makeTestDeployment(cr) + assert.NoError(t, err) resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) assert.NoError(t, err) // Verify Deployment was deleted @@ -304,13 +319,14 @@ func TestReconcilePrincipalDeployment_DeploymentExists_AgentNotSet(t *testing.T) saName := generateAgentResourceName(cr.Name, testCompName) // Create existing Deployment - existingDeployment := makeTestDeployment(cr) + existingDeployment, err := makeTestDeployment(cr) + assert.NoError(t, err) resObjs := []client.Object{cr, existingDeployment} sch := makeTestReconcilerScheme() cl := makeTestReconcilerClient(sch, resObjs) - err := ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) + err = ReconcilePrincipalDeployment(cl, testCompName, saName, cr, sch) assert.NoError(t, err) // Verify Deployment was deleted diff --git a/controllers/argoutil/tls.go b/controllers/argoutil/tls.go index 0142e1a5c..0b5b6c473 100644 --- a/controllers/argoutil/tls.go +++ b/controllers/argoutil/tls.go @@ -17,6 +17,7 @@ package argoutil import ( "crypto/rand" "crypto/rsa" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" @@ -24,10 +25,12 @@ import ( "fmt" "math" "math/big" + "strings" "time" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/argoproj-labs/argocd-operator/common" ) @@ -122,3 +125,286 @@ func NewSignedCertificate(cfg *certmanagerv1.CertificateSpec, dnsNames []string, } return x509.ParseCertificate(certDERBytes) } + +// -------------------- Common Helpers -------------------- + +const ( + defaultTLSMin = "1.3" + defaultTLSMax = "1.3" + defaultAgentTLSMin = "tls1.3" + defaultAgentTLSMax = "tls1.3" + defaultRedisTLSProtocol = "TLSv1.3" +) + +func resolveTLSVersions(min, max, defMin, defMax string) (string, string) { + if min == "" { + min = defMin + } + if max == "" { + max = defMax + } + return min, max +} + +// -------------------- TLS Version Maps -------------------- + +var ( + supportedTLSVersions = map[string]uint16{ + "1.1": tls.VersionTLS11, + "1.2": tls.VersionTLS12, + "1.3": tls.VersionTLS13, + } + + tlsVersionNames = map[uint16]string{ + tls.VersionTLS11: "1.1", + tls.VersionTLS12: "1.2", + tls.VersionTLS13: "1.3", + } + + // Precompute once instead of every validation call + supportedCipherSuites = buildCipherSuiteMap() +) + +func buildCipherSuiteMap() map[string]*tls.CipherSuite { + m := make(map[string]*tls.CipherSuite) + for _, cs := range tls.CipherSuites() { + m[cs.Name] = cs + } + return m +} + +// -------------------- TLS Version Helpers -------------------- +func TLSVersionName(version uint16) string { + if name, ok := tlsVersionNames[version]; ok { + return name + } + return fmt.Sprintf("unknown (0x%04x)", version) +} + +func ParseTLSVersion(v string) (uint16, error) { + if v == "" { + return 0, nil + } + val, ok := supportedTLSVersions[v] + if !ok { + return 0, fmt.Errorf("unsupported TLS version: %s", v) + } + return val, nil +} + +// -------------------- TLS Validation -------------------- +func ValidateTLSConfig(minVersion, maxVersion uint16, cipherSuites []string) error { + // Validate version range + if minVersion != 0 && maxVersion != 0 && minVersion > maxVersion { + return fmt.Errorf( + "minimum TLS version (%s) cannot be higher than maximum TLS version (%s)", + TLSVersionName(minVersion), + TLSVersionName(maxVersion), + ) + } + + // No cipher validation needed + if len(cipherSuites) == 0 { + return nil + } + for _, name := range cipherSuites { + name = strings.TrimSpace(name) + cs, ok := supportedCipherSuites[name] + if !ok { + return fmt.Errorf("unsupported cipher suite: %s", name) + } + // TLS 1.3 ciphers don't need compatibility validation + if minVersion == tls.VersionTLS13 { + continue + } + if !isCipherCompatible(cs, minVersion, maxVersion) { + return fmt.Errorf("cipher suite %s is not compatible with TLS versions [%s - %s]", name, TLSVersionName(minVersion), TLSVersionName(maxVersion)) + } + } + + return nil +} + +func isCipherCompatible(cs *tls.CipherSuite, minVersion, maxVersion uint16) bool { + for _, v := range cs.SupportedVersions { + if (minVersion == 0 || v >= minVersion) && + (maxVersion == 0 || v <= maxVersion) { + return true + } + } + return false +} + +func joinCiphers(cipherSuites []string) string { + if len(cipherSuites) == 0 { + return "" + } + return strings.Join(cipherSuites, ":") +} + +// -------------------- Canonical Normalization -------------------- + +// Used ONLY for parsing/validation +func normalizeTLSVersionForParsing(v string) string { + v = strings.TrimSpace(strings.ToLower(v)) + switch v { + case "1.1", "tls1.1", "tlsv1.1": + return "1.1" + case "1.2", "tls1.2", "tlsv1.2": + return "1.2" + case "1.3", "tls1.3", "tlsv1.3": + return "1.3" + default: + return "" + } +} + +// Output formatters (component-specific) +func formatTLSVersionForAgent(v string) string { + switch v { + case "1.1": + return "tls1.1" + case "1.2": + return "tls1.2" + case "1.3": + return "tls1.3" + default: + return "" + } +} + +func formatTLSVersionForArgoCD(v string) string { + return v // already in "1.x" +} + +func formatTLSVersionForRedis(v string) string { + switch v { + case "1.1": + return "TLSv1.1" + case "1.2": + return "TLSv1.2" + case "1.3": + return "TLSv1.3" + default: + return "" + } +} + +func buildRedisProtocols(min, max string) []string { + order := []string{"TLSv1.1", "TLSv1.2", "TLSv1.3"} + var result []string + start := false + for _, v := range order { + if v == min { + start = true + } + if start { + result = append(result, v) + } + if v == max { + break + } + } + return result +} + +func validateAndParseTLS(tlsCfg *argoproj.ArgoCDTlsConfig) (string, string, error) { + if tlsCfg == nil { + return "", "", nil + } + minStr := normalizeTLSVersionForParsing(tlsCfg.MinVersion) + maxStr := normalizeTLSVersionForParsing(tlsCfg.MaxVersion) + minVer, err := ParseTLSVersion(minStr) + if err != nil { + return "", "", fmt.Errorf("invalid min TLS version: %w", err) + } + maxVer, err := ParseTLSVersion(maxStr) + if err != nil { + return "", "", fmt.Errorf("invalid max TLS version: %w", err) + } + if err := ValidateTLSConfig(minVer, maxVer, tlsCfg.CipherSuites); err != nil { + return "", "", fmt.Errorf("invalid TLS configuration: %w", err) + } + return minStr, maxStr, nil +} + +func BuildArgoCDAgentTLSArgs(tls *argoproj.ArgoCDTlsConfig, args map[string]string) (map[string]string, error) { + if tls == nil { + args["--tlsminversion"] = defaultAgentTLSMin + args["--tlsmaxversion"] = defaultAgentTLSMax + args["--tlsciphers"] = "" + return args, nil + } + minStr, maxStr, err := validateAndParseTLS(tls) + if err != nil { + return nil, err + } + minStr, maxStr = resolveTLSVersions(minStr, maxStr, defaultTLSMin, defaultTLSMax) + args["--tlsminversion"] = formatTLSVersionForAgent(minStr) + args["--tlsmaxversion"] = formatTLSVersionForAgent(maxStr) + if ciphers := joinCiphers(tls.CipherSuites); ciphers != "" { + args["--tlsciphers"] = ciphers + } + return args, nil +} + +func BuildTLSArgs(tls *argoproj.ArgoCDTlsConfig) ([]string, error) { + if tls == nil { + return []string{ + "--tlsminversion", defaultTLSMin, + "--tlsmaxversion", defaultTLSMax, + }, nil + } + minStr, maxStr, err := validateAndParseTLS(tls) + if err != nil { + return nil, err + } + minStr, maxStr = resolveTLSVersions(minStr, maxStr, defaultTLSMin, defaultTLSMax) + args := []string{ + "--tlsminversion", formatTLSVersionForArgoCD(minStr), + "--tlsmaxversion", formatTLSVersionForArgoCD(maxStr), + } + if ciphers := joinCiphers(tls.CipherSuites); ciphers != "" { + args = append(args, "--tlsciphers", ciphers) + } + return args, nil +} + +func BuildRedisArgs(tls *argoproj.ArgoCDTlsConfig) ([]string, error) { + if tls == nil { + return []string{"--tls-protocols", defaultRedisTLSProtocol}, nil + } + minStr, maxStr, err := validateAndParseTLS(tls) + if err != nil { + return nil, err + } + minStr, maxStr = resolveTLSVersions(minStr, maxStr, "1.3", "1.3") + min := formatTLSVersionForRedis(minStr) + max := formatTLSVersionForRedis(maxStr) + protocols := buildRedisProtocols(min, max) + var args []string + if len(protocols) > 0 { + args = append(args, "--tls-protocols", strings.Join(protocols, " ")) + } + // Determine enabled TLS versions + hasTLS12OrBelow := false + hasTLS13 := false + for _, p := range protocols { + switch p { + case "TLSv1.1", "TLSv1.2": + hasTLS12OrBelow = true + case "TLSv1.3": + hasTLS13 = true + } + } + + if ciphers := joinCiphers(tls.CipherSuites); ciphers != "" { + if hasTLS12OrBelow { + args = append(args, "--tls-ciphers", ciphers) + } + if hasTLS13 { + args = append(args, "--tls-ciphersuites", ciphers) + } + } + return args, nil +} diff --git a/deploy/olm-catalog/argocd-operator/0.19.0/argocd-operator.v0.19.0.clusterserviceversion.yaml b/deploy/olm-catalog/argocd-operator/0.19.0/argocd-operator.v0.19.0.clusterserviceversion.yaml index 5ea35331e..0e9007a4a 100644 --- a/deploy/olm-catalog/argocd-operator/0.19.0/argocd-operator.v0.19.0.clusterserviceversion.yaml +++ b/deploy/olm-catalog/argocd-operator/0.19.0/argocd-operator.v0.19.0.clusterserviceversion.yaml @@ -257,7 +257,7 @@ metadata: capabilities: Deep Insights categories: Integration & Delivery certified: "false" - createdAt: "2026-04-10T16:59:38Z" + createdAt: "2026-04-13T18:36:00Z" description: Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. operators.operatorframework.io/builder: operator-sdk-v1.35.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 diff --git a/deploy/olm-catalog/argocd-operator/0.19.0/argoproj.io_argocds.yaml b/deploy/olm-catalog/argocd-operator/0.19.0/argoproj.io_argocds.yaml index 43a1a1ec8..61f3b95ea 100644 --- a/deploy/olm-catalog/argocd-operator/0.19.0/argoproj.io_argocds.yaml +++ b/deploy/olm-catalog/argocd-operator/0.19.0/argoproj.io_argocds.yaml @@ -11920,6 +11920,38 @@ spec: description: SecretName is The name of the secret containing the TLS certificate and key. type: string + tlsConfig: + description: TLS configuration for the Principal component. + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object type: object type: object type: object @@ -18662,6 +18694,39 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + tlsConfig: + description: TlsConfig defines the TLS configuration for the Redis + server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object version: description: Version is the Redis container image tag. type: string @@ -22246,6 +22311,38 @@ spec: x-kubernetes-map-type: atomic type: array type: object + tlsConfig: + description: TLS configuration for the repo server + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object verifytls: description: VerifyTLS defines whether repo server API should be accessed using strict TLS validation @@ -27926,6 +28023,38 @@ spec: - name type: object type: array + tlsConfig: + description: TLS configuration for the Argo CD Server component + properties: + cipherSuites: + items: + type: string + type: array + maxVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + - tls1.1 + - tls1.2 + - tls1.3 + type: string + minVersion: + enum: + - "1.1" + - "1.2" + - "1.3" + - tls1.1 + - tls1.2 + - tls1.3 + - TLSv1.1 + - TLSv1.2 + - TLSv1.3 + type: string + type: object volumeMounts: description: VolumeMounts adds volumeMounts to the Argo CD Server container. diff --git a/tests/ginkgo/parallel/1-066_validate_redis_secure_comm_no_autotls_no_ha_test.go b/tests/ginkgo/parallel/1-066_validate_redis_secure_comm_no_autotls_no_ha_test.go index 0cf8429a2..9cc4a860f 100644 --- a/tests/ginkgo/parallel/1-066_validate_redis_secure_comm_no_autotls_no_ha_test.go +++ b/tests/ginkgo/parallel/1-066_validate_redis_secure_comm_no_autotls_no_ha_test.go @@ -147,8 +147,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { By("expecting redis-server to have desired container process command/arguments") - expectedString := "--save \"\" --appendonly no --aclfile /app/config/redis-auth/users.acl --tls-port 6379 --port 0 --tls-cert-file /app/config/redis/tls/tls.crt --tls-key-file /app/config/redis/tls/tls.key --tls-auth-clients no" - + expectedString := "--save \"\" --appendonly no --aclfile /app/config/redis-auth/users.acl" + " --tls-protocols TLSv1.3" + " --tls-port 6379 --port 0" + " --tls-cert-file /app/config/redis/tls/tls.crt" + " --tls-key-file /app/config/redis/tls/tls.key --tls-auth-clients no" if !fixture.IsUpstreamOperatorTests() { // Downstream operator adds these arguments expectedString = "redis-server --protected-mode no " + expectedString diff --git a/tests/ginkgo/sequential/1-143_validate_deployment_Env_Args_For_Tls_Configuration_test.go b/tests/ginkgo/sequential/1-143_validate_deployment_Env_Args_For_Tls_Configuration_test.go new file mode 100644 index 000000000..ff3e388d1 --- /dev/null +++ b/tests/ginkgo/sequential/1-143_validate_deployment_Env_Args_For_Tls_Configuration_test.go @@ -0,0 +1,816 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +*/ + +package sequential + +import ( + "context" + "os" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture" + osFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/os" + "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/utils" +) + +var _ = Describe("Validate Deployment Env Args For TLS Configuration", func() { + const ( + argocdNamespace = "test-tls-argocd" + argocdInstanceName = "example-argocd" + ) + var ( + c client.Client + ctx context.Context + ) + BeforeEach(func() { + fixture.EnsureSequentialCleanSlate() + c, _ = utils.GetE2ETestKubeClient() + ctx = context.Background() + }) + BeforeEach(func() { + if fixture.EnvLocalRun() { + Skip("This test is known not to work when running gitops operator locally") + } + }) + // --- Helper: Extract TLS values from args --- + getTLSValues := func(args []string) (min string, max string, hasMin bool, hasMax bool, hasCiphers bool, ciphers string) { + for i := 0; i < len(args); i++ { + arg := args[i] + // handle --tlsminversion + if arg == "--tlsminversion" { + hasMin = true + if i+1 < len(args) { + min = args[i+1] + } + } + // handle --tlsmaxversion + if arg == "--tlsmaxversion" { + hasMax = true + if i+1 < len(args) { + max = args[i+1] + } + } + if arg == "--tlsciphers" { + hasCiphers = true + if i+1 < len(args) { + ciphers = args[i+1] + } + } + // handle --tlsminversion=value + if len(arg) > len("--tlsminversion=") && arg[:len("--tlsminversion=")] == "--tlsminversion=" { + hasMin = true + min = arg[len("--tlsminversion="):] + } + // handle --tlsmaxversion=value + if len(arg) > len("--tlsmaxversion=") && arg[:len("--tlsmaxversion=")] == "--tlsmaxversion=" { + hasMax = true + max = arg[len("--tlsmaxversion="):] + } + if len(arg) > len("--tlsciphers=") && arg[:len("--tlsciphers=")] == "--tlsciphers=" { + hasCiphers = true + ciphers = arg[len("--tlsciphers="):] + } + } + return + } + + Context("When the ArgoCD instance is created with default TLS settings", func() { + It("should validate default TLS values and updates on RepoServer, Server and Redis Deployments", func() { + By("creating namespace") + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: argocdNamespace, + }, + } + Expect(c.Create(ctx, ns)).To(Succeed()) + + By("generating a test certificate to use with redis, using openssl") + redis_crt_File, err := os.CreateTemp("", "redis.crt") + Expect(err).ToNot(HaveOccurred()) + + redis_key_File, err := os.CreateTemp("", "redis.key") + Expect(err).ToNot(HaveOccurred()) + + openssl_test_File, err := os.CreateTemp("", "openssl_test.cnf") + Expect(err).ToNot(HaveOccurred()) + + opensslTestCNFContents := "\n[SAN]\nsubjectAltName=DNS:argocd-redis." + argocdNamespace + ".svc.cluster.local\n[req]\ndistinguished_name=req" + + err = os.WriteFile(openssl_test_File.Name(), ([]byte)(opensslTestCNFContents), 0666) + Expect(err).ToNot(HaveOccurred()) + + _, err = osFixture.ExecCommandWithOutputParam(false, true, "openssl", "req", "-new", "-x509", "-sha256", + "-subj", "/C=XX/ST=XX/O=Testing/CN=redis", + "-reqexts", "SAN", + "-extensions", "SAN", + "-config", openssl_test_File.Name(), + "-keyout", redis_key_File.Name(), + "-out", redis_crt_File.Name(), + "-newkey", "rsa:4096", + "-nodes", + "-days", "10", + ) + Expect(err).ToNot(HaveOccurred()) + + By("creating argocd-operator-redis-tls secret from that cert") + _, err = osFixture.ExecCommand("kubectl", "create", "secret", "tls", "argocd-operator-redis-tls", "--key="+redis_key_File.Name(), "--cert="+redis_crt_File.Name(), "-n", argocdNamespace) + Expect(err).ToNot(HaveOccurred()) + + By("adding argo cd label to argocd-operator-redis-tls secret") + _, err = osFixture.ExecCommand("kubectl", "annotate", "secret", "argocd-operator-redis-tls", "argocds.argoproj.io/name=argocd", "-n", argocdNamespace) + Expect(err).ToNot(HaveOccurred()) + + By("creating ArgoCD instance") + argo := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: argocdInstanceName, + Namespace: argocdNamespace, + }, + Spec: argov1beta1api.ArgoCDSpec{}, + } + Expect(c.Create(ctx, argo)).To(Succeed()) + + By("waiting for ArgoCD to be available") + Eventually(func() error { + return c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, &argov1beta1api.ArgoCD{}) + }, 2*time.Minute, 5*time.Second).Should(Succeed()) + defer func() { + By("cleaning up resources") + _ = c.Delete(ctx, argo) + _ = c.Delete(ctx, ns) + os.Remove(redis_crt_File.Name()) + os.Remove(redis_key_File.Name()) + os.Remove(openssl_test_File.Name()) + }() + coreDeployments := []string{ + "example-argocd-server", + "example-argocd-repo-server", + } + // --- Validate default TLS values --- + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + By("waiting for deployment " + deploymentName) + Eventually(func() error { + return c.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment) + }, 2*time.Minute, 2*time.Second).Should(Succeed()) + + By("validating default TLS args in " + deploymentName) + Eventually(func() bool { + if err := c.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, hasMin, hasMax, _, _ := getTLSValues(container.Args) + if !hasMin || !hasMax { + continue + } + if min != "1.3" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.3, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + GinkgoWriter.Printf("%s default TLS OK: min=%s max=%s\n", deploymentName, min, max) + return true + } + return false + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + } + By("validating default TLS args in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + } + + // --- Print results (always helpful in debugging) + if hasCiphers || tlsCipherSuites != "" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be empty", tlsCipherSuites) + return false + } + if hasCiphersTLS2 || tlsCiphers != "" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be empty", tlsCiphers) + return false + } + + if !hasProtocols || tlsProtocols != "TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + // --- Update TLS config --- + By("updating TLS config in ArgoCD CR For RepoServer, Server and Redis") + Expect(c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo)).To(Succeed()) + argo.Spec.Repo.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "1.2", + MaxVersion: "1.3", + } + argo.Spec.Server.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "1.2", + MaxVersion: "1.3", + } + argo.Spec.Redis.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "1.1", + MaxVersion: "1.3", + } + Expect(c.Update(ctx, argo)).To(Succeed()) + time.Sleep(5 * time.Second) + // --- Validate updated TLS values --- + By("validating updated TLS args For RepoServer and Server") + Eventually(func() bool { + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, + types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + valid := false + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, hasMin, hasMax, _, _ := getTLSValues(container.Args) + if !hasMin || !hasMax { + continue + } + if min != "1.2" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.2, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + GinkgoWriter.Printf("%s updated TLS OK: min=%s max=%s\n", deploymentName, min, max) + valid = true + } + if !valid { + return false + } + } + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue(), "all deployments should have updated TLS configuration") + + By("Validating Updated TLS args in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + // --- Handle "--tls-ciphers " + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + } + + // --- Print results (always helpful in debugging) + if hasCiphers || tlsCipherSuites != "" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be empty", tlsCipherSuites) + return false + } + if hasCiphersTLS2 || tlsCiphers != "" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be empty", tlsCiphers) + return false + } + + if !hasProtocols || tlsProtocols != "TLSv1.1 TLSv1.2 TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.1 TLSv1.2 TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + By("Update single cipherSuites for RepoServer, Server and Redis Deployments") + Expect(c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo)).To(Succeed()) + argo.Spec.Repo.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + argo.Spec.Server.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + argo.Spec.Redis.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "1.2", + MaxVersion: "1.3", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + } + Expect(c.Update(ctx, argo)).To(Succeed()) + + time.Sleep(5 * time.Second) + // --- Validate updated TLS values --- + By("validating updated TLS single CipherSuites For RepoServer and Server") + Eventually(func() bool { + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, + types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + valid := false + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, hasMin, hasMax, hasCiphers, ciphers := getTLSValues(container.Args) + if !hasMin || !hasMax || !hasCiphers { + continue + } + if min != "1.2" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.2, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + if ciphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" { + GinkgoWriter.Printf("%s: expected tlsciphers=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, got %s\n", deploymentName, ciphers) + return false + } + GinkgoWriter.Printf("%s updated TLS OK: min=%s max=%s\n ciphers=%s\n", deploymentName, min, max, ciphers) + valid = true + } + if !valid { + return false + } + } + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue(), "all deployments should have updated TLS configuration") + + By("validating TLS single CipherSuite in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + + // --- Handle "--tls-ciphers " + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + } + + // --- Print results (always helpful in debugging) + if hasCiphers && tlsCipherSuites != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, but got %q\n", tlsCipherSuites, tlsCipherSuites) + return false + } + if hasCiphersTLS2 && tlsCiphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, but got %q\n", tlsCiphers, tlsCiphers) + return false + } + if !hasProtocols || tlsProtocols != "TLSv1.2 TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.2 TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + By("Check Any Format of passing tls should work for all deployments") + By("updating TLS config in any format in argocd CR For RepoServer, Server and Redis") + Expect(c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo)).To(Succeed()) + argo.Spec.Repo.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "TLSv1.2", + MaxVersion: "TLSv1.3", + CipherSuites: []string{}, + } + argo.Spec.Server.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "TLSv1.2", + MaxVersion: "TLSv1.3", + CipherSuites: []string{}, + } + argo.Spec.Redis.TlsConfig = &argov1beta1api.ArgoCDTlsConfig{ + MinVersion: "TLSv1.2", + MaxVersion: "TLSv1.3", + CipherSuites: []string{}, + } + Expect(c.Update(ctx, argo)).To(Succeed()) + time.Sleep(5 * time.Second) + // --- Validate updated TLS values --- + By("validating updated TLS Config For RepoServer, Server and Redis") + Eventually(func() bool { + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, + types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + valid := false + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, hasMin, hasMax, _, _ := getTLSValues(container.Args) + if !hasMin || !hasMax { + continue + } + if min != "1.2" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.2, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + GinkgoWriter.Printf("%s updated TLS OK: min=%s max=%s\n", deploymentName, min, max) + valid = true + } + if !valid { + return false + } + } + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue(), "all deployments should have updated TLS configuration") + + By("validating TLS args of any format in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + + } + + // --- Print results (always helpful in debugging) + if hasCiphers || tlsCipherSuites != "" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be empty", tlsCipherSuites) + return false + } + if hasCiphersTLS2 || tlsCiphers != "" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be empty", tlsCiphers) + return false + } + if !hasProtocols || tlsProtocols != "TLSv1.2 TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.2 TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + By("Update Two cipherSuites for RepoServer, Server and Redis Deployments") + Expect(c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo)).To(Succeed()) + argo.Spec.Repo.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + argo.Spec.Server.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + argo.Spec.Redis.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + Expect(c.Update(ctx, argo)).To(Succeed()) + + time.Sleep(5 * time.Second) + // --- Validate updated TLS values --- + By("validating updated TLS double CipherSuites For RepoServer and Server") + Eventually(func() bool { + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, + types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + valid := false + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, _, _, _, ciphers := getTLSValues(container.Args) + if min != "1.2" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.2, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + if ciphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf("%s: expected tlsciphers=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, got %s\n", deploymentName, ciphers) + return false + } + GinkgoWriter.Printf("%s updated TLS OK: min=%s max=%s\n ciphers=%s\n", deploymentName, min, max, ciphers) + valid = true + } + if !valid { + return false + } + } + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue(), "all deployments should have updated TLS configuration") + + By("validating TLS double CipherSuite in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + } + + // --- Print results (always helpful in debugging) + if hasCiphers && tlsCipherSuites != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, but got %q\n", tlsCipherSuites, tlsCipherSuites) + return false + } + + if hasCiphersTLS2 && tlsCiphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, but got %q\n", tlsCiphers, tlsCiphers) + return false + } + + if !hasProtocols || tlsProtocols != "TLSv1.2 TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.2 TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + By("Check The deployments doesnt have invalid values and check the argocd CR status for Error") + Expect(c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo)).To(Succeed()) + argo.Spec.Repo.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + argo.Spec.Server.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + argo.Spec.Redis.TlsConfig.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + Expect(c.Update(ctx, argo)).To(Succeed()) + time.Sleep(5 * time.Second) + Eventually(func() *metav1.Condition { + err := c.Get(ctx, types.NamespacedName{Name: argocdInstanceName, Namespace: argocdNamespace}, argo) + if err != nil { + return nil + } + for _, cond := range argo.Status.Conditions { + if cond.Reason == "ErrorOccurred" && cond.Message == "invalid TLS configuration: unsupported cipher suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + return &cond + } + } + return nil + }, 30*time.Second, 1*time.Second).ShouldNot(BeNil(), "Expected TLS validation error condition not found") + + // --- Validate updated TLS values --- + By("validating updated TLS invalid double CipherSuites For RepoServer, Server and Redis") + Eventually(func() bool { + for _, deploymentName := range coreDeployments { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, + types.NamespacedName{Name: deploymentName, Namespace: argocdNamespace}, deployment); err != nil { + return false + } + valid := false + for _, container := range deployment.Spec.Template.Spec.Containers { + min, max, _, _, _, ciphers := getTLSValues(container.Args) + if min != "1.2" { + GinkgoWriter.Printf("%s: expected tlsminversion=1.2, got %s\n", deploymentName, min) + return false + } + if max != "1.3" { + GinkgoWriter.Printf("%s: expected tlsmaxversion=1.3, got %s\n", deploymentName, max) + return false + } + if ciphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf("%s: expected tlsciphers=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, got %s\n", deploymentName, ciphers) + return false + } + GinkgoWriter.Printf("%s not updated TLS invalid values OK: min=%s max=%s\n ciphers=%s\n", deploymentName, min, max, ciphers) + valid = true + } + if !valid { + return false + } + } + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue(), "all deployments should have updated TLS configuration") + + By("validating TLS double CipherSuite in Redis deployment") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: "example-argocd-redis", Namespace: argocdNamespace}, deployment); err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return false + } + args := deployment.Spec.Template.Spec.Containers[0].Args + var tlsProtocols string + var tlsCipherSuites string + var tlsCiphers string + hasProtocols := false + hasCiphers := false + hasCiphersTLS2 := false + for i := 0; i < len(args); i++ { + arg := args[i] + // --- Handle "--tls-protocols " + if arg == "--tls-protocols" { + hasProtocols = true + if i+1 < len(args) { + tlsProtocols = args[i+1] + } + } + // --- Handle "--tls-ciphersuites " + if arg == "--tls-ciphersuites" { + hasCiphers = true + if i+1 < len(args) { + tlsCipherSuites = args[i+1] + } + } + + if arg == "--tls-ciphers" { + hasCiphersTLS2 = true + if i+1 < len(args) { + tlsCiphers = args[i+1] + } + } + + } + + // --- Print results (always helpful in debugging) + if hasCiphers && tlsCipherSuites != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf(" --tls-ciphersuites=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, but got %q\n", tlsCipherSuites, tlsCipherSuites) + return false + } + + if hasCiphersTLS2 && tlsCiphers != "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { + GinkgoWriter.Printf(" --tls-ciphers=%q\n should be TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, but got %q\n", tlsCiphers, tlsCiphers) + return false + } + + if !hasProtocols || tlsProtocols != "TLSv1.2 TLSv1.3" { + GinkgoWriter.Printf("%s: expected --tls-protocols=TLSv1.2 TLSv1.3, got %s\n", deployment.Name, tlsProtocols) + return false + } + GinkgoWriter.Print("TLS Invalid values not populated") + GinkgoWriter.Printf("%s TLS args protocol value: %s\n", deployment.Name, tlsProtocols) + GinkgoWriter.Printf("%s TLS args ciphersuites value: %s\n", deployment.Name, tlsCipherSuites) + GinkgoWriter.Printf("%s TLS args ciphers value: %s\n", deployment.Name, tlsCiphers) + return true + }, 60*time.Second, 2*time.Second).Should(BeTrue()) + + }) + }) +})