diff --git a/Makefile b/Makefile index 7323fa44..4fc46fd9 100644 --- a/Makefile +++ b/Makefile @@ -179,15 +179,15 @@ run: manifests generate fmt vet ## Run a controller from your host. # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ -.PHONY: docker-build -docker-build: ## Build docker image with the manager. +.PHONY: image-build +image-build: ## Build operator image. $(CONTAINER_TOOL) build -t ${IMG} . -.PHONY: docker-push -docker-push: ## Push docker image with the manager. +.PHONY: image-push +image-push: ## Push operator image. $(CONTAINER_TOOL) push ${IMG} -# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# PLATFORMS defines the target platforms for the operator image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ @@ -198,10 +198,10 @@ PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - - $(CONTAINER_TOOL) buildx create --name external-secrets-operator-builder - $(CONTAINER_TOOL) buildx use external-secrets-operator-builder - - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - - $(CONTAINER_TOOL) buildx rm external-secrets-operator-builder + - docker buildx create --name external-secrets-operator-builder + docker buildx use external-secrets-operator-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm external-secrets-operator-builder rm Dockerfile.cross .PHONY: build-installer @@ -240,7 +240,7 @@ LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) -## Location to story temp outputs +## Location to store temp outputs OUTPUTS_PATH ?= $(shell pwd)/_output $(OUTPUTS_PATH): mkdir -p $(OUTPUTS_PATH) @@ -293,16 +293,16 @@ govulncheck: $(LOCALBIN) ## Download govulncheck locally if necessary. ginkgo: $(LOCALBIN) ## Download ginkgo locally if necessary. $(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo) -# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# go-install-tool will 'go install' any package with custom target and name of the binary. # $1 - target path with name of binary # $2 - package url which can be installed define go-install-tool @{ \ set -e; \ package=$(2) ;\ -echo "Downloading $${package}" ;\ +echo "Installing $${package}" ;\ rm -f $(1) || true ;\ -GOBIN=$(LOCALBIN) go install $${package} ;\ +GOBIN=$(LOCALBIN) GOFLAGS="-mod=vendor" go install $${package} ;\ } endef @@ -352,11 +352,11 @@ bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metada .PHONY: bundle-build bundle-build: ## Build the bundle image. - docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + $(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) . .PHONY: bundle-push bundle-push: ## Push the bundle image. - $(MAKE) docker-push IMG=$(BUNDLE_IMG) + $(CONTAINER_TOOL) push $(BUNDLE_IMG) .PHONY: opm OPM = $(LOCALBIN)/opm @@ -392,12 +392,12 @@ endif # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator .PHONY: catalog-build catalog-build: opm ## Build a catalog image. - $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) + $(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) # Push the catalog image. .PHONY: catalog-push catalog-push: ## Push a catalog image. - $(MAKE) docker-push IMG=$(CATALOG_IMG) + $(CONTAINER_TOOL) push $(CATALOG_IMG) ## verify the changes are working as expected. .PHONY: verify @@ -419,15 +419,30 @@ docs: crd-ref-docs ## perform vulnerabilities scan using govulncheck. .PHONY: govulnscan -#The ignored vulnerabilities are not in the operator code, but in the vendored packages. +# The ignored vulnerabilities are not in the operator code, but in the vendored packages. +# Each vulnerability ID corresponds to a specific issue that has been reviewed and deemed +# acceptable for the current vendored dependencies. # - https://pkg.go.dev/vuln/GO-2025-3956 # - https://pkg.go.dev/vuln/GO-2025-3547 # - https://pkg.go.dev/vuln/GO-2025-3521 -KNOWN_VULNERABILITIES:="GO-2025-3547|GO-2025-3521|GO-2025-3956|GO-2025-3915" +KNOWN_VULNERABILITIES=GO-2025-3956|GO-2025-3547|GO-2025-3521 govulnscan: govulncheck $(OUTPUTS_PATH) ## Run govulncheck - - $(GOVULNCHECK) ./... > $(OUTPUTS_PATH)/govulcheck.results 2>&1 - $(eval reported_vulnerabilities = $(strip $(shell grep "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results | ([ -n $KNOWN_VULNERABILITIES ] && grep -Ev $(KNOWN_VULNERABILITIES) || cat) | wc -l))) - @(if [ $(reported_vulnerabilities) -ne 0 ]; then echo -e "\n-- ERROR -- $(reported_vulnerabilities) new vulnerabilities reported, please check\n"; exit 1; fi) + @echo "Running govulncheck vulnerability scan..." + @$(GOVULNCHECK) ./... > $(OUTPUTS_PATH)/govulcheck.results 2>&1 || true + @grep -q "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results || { \ + echo "-- ERROR -- govulncheck may have failed to run; see $(OUTPUTS_PATH)/govulcheck.results"; exit 1; } + @echo "Filtering known vulnerabilities and counting new ones..." + $(eval reported_vulnerabilities = $(strip $(shell grep "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results | grep -Ev "$(KNOWN_VULNERABILITIES)" | wc -l))) + @echo "Found $(reported_vulnerabilities) new vulnerabilities (excluding known issues)" + @(if [ $(reported_vulnerabilities) -ne 0 ]; then \ + echo ""; \ + echo "-- ERROR -- $(reported_vulnerabilities) new vulnerabilities reported"; \ + echo "Please review $(OUTPUTS_PATH)/govulcheck.results for details"; \ + echo ""; \ + exit 1; \ + else \ + echo "✓ Vulnerability scan passed - no new issues found"; \ + fi) # Utilize controller-runtime provided envtest for API integration test .PHONY: test-apis ## Run only the api integration tests. diff --git a/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml b/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml index ac83606a..aa493a04 100644 --- a/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml +++ b/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml @@ -220,7 +220,7 @@ metadata: categories: Security console.openshift.io/disable-operand-delete: "true" containerImage: openshift.io/external-secrets-operator:latest - createdAt: "2025-10-09T11:13:16Z" + createdAt: "2025-10-09T14:41:51Z" features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" features.operators.openshift.io/csi: "false" @@ -756,12 +756,9 @@ spec: initialDelaySeconds: 5 periodSeconds: 10 resources: - limits: - cpu: 500m - memory: 128Mi requests: - cpu: 10m - memory: 64Mi + cpu: 100m + memory: 1Gi securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/cmd/external-secrets-operator/main.go b/cmd/external-secrets-operator/main.go index 714f443c..625c90c0 100644 --- a/cmd/external-secrets-operator/main.go +++ b/cmd/external-secrets-operator/main.go @@ -156,7 +156,11 @@ func main() { metricsServerOptions.KeyName = metricsKeyFileName } metricsTLSOpts = append(metricsTLSOpts, func(c *tls.Config) { - certPool := x509.NewCertPool() + certPool, err := x509.SystemCertPool() + if err != nil { + setupLog.Info("unable to load system certificate pool", "error", err) + certPool = x509.NewCertPool() + } openshiftCACert, err := os.ReadFile(openshiftCACertificateFile) if err != nil { setupLog.Error(err, "failed to read OpenShift CA certificate") diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 6eb021e1..a0b7b80f 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -119,11 +119,8 @@ spec: initialDelaySeconds: 5 periodSeconds: 10 resources: - limits: - cpu: 500m - memory: 128Mi requests: - cpu: 10m - memory: 64Mi + cpu: 100m + memory: 1Gi serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/pkg/controller/common/utils.go b/pkg/controller/common/utils.go index f49f4b65..3f19bfa9 100644 --- a/pkg/controller/common/utils.go +++ b/pkg/controller/common/utils.go @@ -224,16 +224,17 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool { return true } for _, desiredVolume := range desired.Spec.Template.Spec.Volumes { - if desiredVolume.Secret != nil && desiredVolume.Secret.Items != nil { + if desiredVolume.Secret != nil { for _, fetchedVolume := range fetched.Spec.Template.Spec.Volumes { - if !reflect.DeepEqual(desiredVolume.Secret.Items, fetchedVolume.Secret.Items) { - return true - } - if desiredVolume.Secret.SecretName != fetchedVolume.Secret.SecretName { - return true + if desiredVolume.Name == fetchedVolume.Name { + if !reflect.DeepEqual(desiredVolume.Secret.Items, fetchedVolume.Secret.Items) { + return true + } + if !reflect.DeepEqual(desiredVolume.Secret.SecretName, fetchedVolume.Secret.SecretName) { + return true + } } } - } } diff --git a/pkg/controller/external_secrets/certificate.go b/pkg/controller/external_secrets/certificate.go index 955d0719..f68c3cfb 100644 --- a/pkg/controller/external_secrets/certificate.go +++ b/pkg/controller/external_secrets/certificate.go @@ -82,6 +82,11 @@ func (r *Reconciler) createOrApplyCertificate(esc *operatorv1alpha1.ExternalSecr func (r *Reconciler) getCertificateObject(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string, fileName string) (*certmanagerv1.Certificate, error) { certificate := common.DecodeCertificateObjBytes(assets.MustAsset(fileName)) + // update the secret name in the Certificate resource of the webhook component. + if fileName == webhookCertificateAssetName { + certificate.Spec.SecretName = certmanagerTLSSecretWebhook + } + updateNamespace(certificate, esc) common.UpdateResourceLabels(certificate, resourceLabels) diff --git a/pkg/controller/external_secrets/constants.go b/pkg/controller/external_secrets/constants.go index a7d76a76..47fada10 100644 --- a/pkg/controller/external_secrets/constants.go +++ b/pkg/controller/external_secrets/constants.go @@ -48,6 +48,10 @@ const ( // externalsecretsDefaultNamespace is the namespace where the `external-secrets` operand required resources // will be created, when ExternalSecretsConfig.Spec.Namespace is not set. externalsecretsDefaultNamespace = "external-secrets" + + // certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different + // name is used to avoiding clash with the secret created by the inbuilt cert-controller component. + certmanagerTLSSecretWebhook = "external-secrets-webhook-cm" ) var ( diff --git a/pkg/controller/external_secrets/deployments.go b/pkg/controller/external_secrets/deployments.go index 113b1285..0f3af330 100644 --- a/pkg/controller/external_secrets/deployments.go +++ b/pkg/controller/external_secrets/deployments.go @@ -123,6 +123,7 @@ func (r *Reconciler) getDeploymentObject(assetName string, esc *operatorv1alpha1 checkInterval = esc.Spec.ApplicationConfig.WebhookConfig.CertificateCheckInterval.Duration.String() } updateWebhookContainerSpec(deployment, image, logLevel, checkInterval) + updateWebhookVolumeConfig(deployment, esc) case certControllerDeploymentAssetName: updateCertControllerContainerSpec(deployment, image, logLevel) case bitwardenDeploymentAssetName: @@ -302,20 +303,31 @@ func (r *Reconciler) updateImageInStatus(esc *operatorv1alpha1.ExternalSecretsCo // argument list for external-secrets deployment resource func updateContainerSpec(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig, image, logLevel string) { - namespace := getOperatingNamespace(esc) + var ( + enableClusterStoreArgFmt = "--enable-cluster-store-reconciler=%s" + enableClusterExternalSecretsArgFmt = "--enable-cluster-external-secret-reconciler=%s" + ) + args := []string{ "--concurrent=1", "--metrics-addr=:8080", fmt.Sprintf("--loglevel=%s", logLevel), "--zap-time-encoding=epoch", "--enable-leader-election=true", - "--enable-cluster-store-reconciler=true", - "--enable-cluster-external-secret-reconciler=true", "--enable-push-secret-reconciler=true", } + // when spec.appConfig.operatingNamespace is configured, which is for restricting the + // external-secrets custom resource reconcile scope to specified namespace, the reconciliation + // of cluster scoped custom resources must also be disabled. + namespace := getOperatingNamespace(esc) if namespace != "" { - args = append(args, fmt.Sprintf("--namespace=%s", namespace)) + args = append(args, fmt.Sprintf("--namespace=%s", namespace), + fmt.Sprintf(enableClusterStoreArgFmt, "false"), + fmt.Sprintf(enableClusterExternalSecretsArgFmt, "false")) + } else { + args = append(args, fmt.Sprintf(enableClusterStoreArgFmt, "true"), + fmt.Sprintf(enableClusterExternalSecretsArgFmt, "true")) } for i, container := range deployment.Spec.Template.Spec.Containers { @@ -399,27 +411,29 @@ func updateBitwardenVolumeConfig(deployment *appsv1.Deployment, esc *operatorv1a } } +func updateWebhookVolumeConfig(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) { + if isCertManagerConfigEnabled(esc) { + updateSecretVolumeConfig(deployment, "certs", certmanagerTLSSecretWebhook) + } +} + func updateSecretVolumeConfig(deployment *appsv1.Deployment, volumeName, secretName string) { - volumeExists := false for i := range deployment.Spec.Template.Spec.Volumes { if deployment.Spec.Template.Spec.Volumes[i].Name == volumeName { - volumeExists = true - } - if deployment.Spec.Template.Spec.Volumes[i].Secret == nil { - deployment.Spec.Template.Spec.Volumes[i].Secret = &corev1.SecretVolumeSource{} + if deployment.Spec.Template.Spec.Volumes[i].Secret == nil { + deployment.Spec.Template.Spec.Volumes[i].Secret = &corev1.SecretVolumeSource{} + } + deployment.Spec.Template.Spec.Volumes[i].Secret.SecretName = secretName + return } - deployment.Spec.Template.Spec.Volumes[i].Secret.SecretName = secretName - break } - if !volumeExists { - deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - }, + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, }, - }) - } + }, + }) }