Преглед изворни кода

feat: apply TLS security profile to the external-secrets deployment (#6617)

* feat: apply APIServer TLS security profile to the external-secrets deployment

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>

* updated the helm template to accept go defaults

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>

* Updated tls min version parameters to use go defaults

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>

* regenerate helm chart README with TLS values

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>

* Validate numeric TLS curve IDs against known CurveID values

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>

---------

Signed-off-by: Siddhi Bhor <sbhor@redhat.com>
Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
Siddhi Bhor пре 3 дана
родитељ
комит
d8298f14da

+ 14 - 4
cmd/controller/certcontroller.go

@@ -18,7 +18,6 @@ limitations under the License.
 package controller
 
 import (
-	"crypto/tls"
 	"os"
 	"time"
 
@@ -95,10 +94,12 @@ var certcontrollerCmd = &cobra.Command{
 			setupLog.Error(nil, "--metrics-auth requires --metrics-secure; bearer tokens over plaintext HTTP is not allowed")
 			os.Exit(1)
 		}
-		// Disable HTTP/2 if not explicitly enabled
-		if !enableHTTP2 {
-			metricsServerOpts.TLSOpts = []func(*tls.Config){disableHTTP2}
+		metricsTLSOpts, err := buildTLSConfigFuncs(tlsCiphers, tlsMinVersion, tlsCurvePreferences, enableHTTP2)
+		if err != nil {
+			setupLog.Error(err, "unable to configure TLS for certcontroller metrics server")
+			os.Exit(1)
 		}
+		metricsServerOpts.TLSOpts = metricsTLSOpts
 
 		mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
 			Scheme:  scheme,
@@ -221,6 +222,15 @@ func init() {
 	certcontrollerCmd.Flags().StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
 	certcontrollerCmd.Flags().StringVar(&zapTimeEncoding, "zap-time-encoding", "epoch", "Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano')")
 	certcontrollerCmd.Flags().DurationVar(&crdRequeueInterval, "crd-requeue-interval", time.Minute*5, "Time duration between reconciling CRDs for new certs")
+	certcontrollerCmd.Flags().StringVar(&tlsCiphers, "tls-ciphers", "", "comma separated list of tls ciphers allowed for the metrics server. "+
+		"This does not apply to TLS 1.3 as the ciphers are selected automatically. "+
+		"Full lists of available ciphers can be found at https://pkg.go.dev/crypto/tls#pkg-constants")
+	certcontrollerCmd.Flags().StringVar(&tlsMinVersion, "tls-min-version", "", "minimum version of TLS supported for the metrics server. "+
+		"If not specified, Go's default minimum version is used. Valid values: 1.0, 1.1, 1.2, 1.3")
+	certcontrollerCmd.Flags().StringSliceVar(&tlsCurvePreferences, "tls-curve-preferences", nil,
+		"ordered list of TLS key exchange curves for the metrics server "+
+			"(for example X25519,CurveP256, or decimal tls.CurveID values supported by this Go toolchain). "+
+			"If omitted, Go defaults are used.")
 	certcontrollerCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false,
 		"If set, HTTP/2 will be enabled for the metrics server")
 }

+ 82 - 3
cmd/controller/root.go

@@ -18,7 +18,9 @@ package controller
 
 import (
 	"crypto/tls"
+	"fmt"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/spf13/cobra"
@@ -52,6 +54,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/cssmetrics"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/ssmetrics"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
 	"github.com/external-secrets/external-secrets/runtime/feature"
 
 	// To allow using gcp auth.
@@ -106,6 +109,7 @@ var (
 	certLookaheadInterval                 time.Duration
 	tlsCiphers                            string
 	tlsMinVersion                         string
+	tlsCurvePreferences                   []string
 	enableHTTP2                           bool
 	allowGenericTargets                   bool
 )
@@ -167,10 +171,12 @@ var rootCmd = &cobra.Command{
 			setupLog.Error(nil, "--metrics-auth requires --metrics-secure; bearer tokens over plaintext HTTP is not allowed")
 			os.Exit(1)
 		}
-		// Disable HTTP/2 if not explicitly enabled
-		if !enableHTTP2 {
-			metricsOpts.TLSOpts = []func(*tls.Config){disableHTTP2}
+		metricsTLSOpts, err := buildTLSConfigFuncs(tlsCiphers, tlsMinVersion, tlsCurvePreferences, enableHTTP2)
+		if err != nil {
+			setupLog.Error(err, "unable to configure TLS for metrics server")
+			os.Exit(1)
 		}
+		metricsOpts.TLSOpts = metricsTLSOpts
 		mgrOpts := ctrl.Options{
 			Scheme:                 scheme,
 			Metrics:                metricsOpts,
@@ -385,6 +391,15 @@ func init() {
 	rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.")
 	rootCmd.Flags().BoolVar(&enableGeneratorState, "enable-generator-state", true, "Whether the Controller should manage GeneratorState")
 	rootCmd.Flags().BoolVar(&enableExtendedMetricLabels, "enable-extended-metric-labels", false, "Enable recommended kubernetes annotations as labels in metrics.")
+	rootCmd.Flags().StringVar(&tlsCiphers, "tls-ciphers", "", "comma separated list of tls ciphers allowed for the metrics server. "+
+		"This does not apply to TLS 1.3 as the ciphers are selected automatically. "+
+		"Full lists of available ciphers can be found at https://pkg.go.dev/crypto/tls#pkg-constants")
+	rootCmd.Flags().StringVar(&tlsMinVersion, "tls-min-version", "", "minimum version of TLS supported for the metrics server. "+
+		"If not specified, Go's default minimum version is used. Valid values: 1.0, 1.1, 1.2, 1.3")
+	rootCmd.Flags().StringSliceVar(&tlsCurvePreferences, "tls-curve-preferences", nil,
+		"ordered list of TLS key exchange curves for the metrics server "+
+			"(for example X25519,CurveP256, or decimal tls.CurveID values supported by this Go toolchain). "+
+			"If omitted, Go defaults are used.")
 	rootCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false,
 		"If set, HTTP/2 will be enabled for the metrics server")
 	rootCmd.Flags().
@@ -399,3 +414,67 @@ func init() {
 func disableHTTP2(cfg *tls.Config) {
 	cfg.NextProtos = []string{"http/1.1"}
 }
+
+// parseTLSCurvePreferences converts human-readable curve names to tls.CurveID values.
+// It accepts well-known names (X25519, CurveP256, CurveP384, CurveP521 and aliases)
+// as well as decimal tls.CurveID values for forward-compat with new Go toolchains.
+func parseTLSCurvePreferences(names []string) ([]tls.CurveID, error) {
+	filtered := make([]string, 0, len(names))
+	for _, n := range names {
+		n = strings.TrimSpace(n)
+		if n == "" {
+			continue
+		}
+		filtered = append(filtered, n)
+	}
+	if len(filtered) == 0 {
+		return nil, nil
+	}
+	return esutils.ParseCurvePreferences(filtered)
+}
+
+// buildTLSConfigFuncs assembles a slice of tls.Config mutators from the current
+// flag values. It is shared across all subcommands (controller, webhook, certcontroller).
+func buildTLSConfigFuncs(ciphers, minVer string, curves []string, http2 bool) ([]func(*tls.Config), error) {
+	var opts []func(*tls.Config)
+
+	if !http2 {
+		opts = append(opts, disableHTTP2)
+	}
+
+	if ciphers != "" {
+		ids, err := getTLSCipherSuitesIDs(ciphers)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse tls ciphers: %w", err)
+		}
+		if len(ids) > 0 {
+			opts = append(opts, func(cfg *tls.Config) {
+				cfg.CipherSuites = ids
+			})
+		}
+	}
+
+	if minVer != "" {
+		ver, err := tlsVersion(minVer)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse tls min version: %w", err)
+		}
+		opts = append(opts, func(cfg *tls.Config) {
+			cfg.MinVersion = ver
+		})
+	}
+
+	if len(curves) > 0 {
+		curveIDs, err := parseTLSCurvePreferences(curves)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse tls curve preferences: %w", err)
+		}
+		if len(curveIDs) > 0 {
+			opts = append(opts, func(cfg *tls.Config) {
+				cfg.CurvePreferences = curveIDs
+			})
+		}
+	}
+
+	return opts, nil
+}

+ 152 - 0
cmd/controller/tls_test.go

@@ -0,0 +1,152 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"crypto/tls"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func applyTLSOpts(opts []func(*tls.Config)) *tls.Config {
+	cfg := &tls.Config{}
+	for _, fn := range opts {
+		fn(cfg)
+	}
+	return cfg
+}
+
+func TestBuildTLSConfigFuncs(t *testing.T) {
+	tests := []struct {
+		name                 string
+		ciphers              string
+		minVer               string
+		curves               []string
+		http2                bool
+		wantErr              bool
+		wantMinVersion       uint16
+		wantCipherSuites     bool
+		wantCurvePreferences bool
+		wantHTTP2Disabled    bool
+	}{
+		{
+			name:              "all empty uses Go defaults, HTTP/2 disabled",
+			wantHTTP2Disabled: true,
+		},
+		{
+			name:              "empty minVersion does not set MinVersion",
+			minVer:            "",
+			wantMinVersion:    0,
+			wantHTTP2Disabled: true,
+		},
+		{
+			name:              "explicit minVersion sets MinVersion",
+			minVer:            "1.3",
+			wantMinVersion:    tls.VersionTLS13,
+			wantHTTP2Disabled: true,
+		},
+		{
+			name:              "explicit minVersion 1.2",
+			minVer:            "1.2",
+			wantMinVersion:    tls.VersionTLS12,
+			wantHTTP2Disabled: true,
+		},
+		{
+			name:              "valid cipher suites are applied",
+			ciphers:           "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+			wantCipherSuites:  true,
+			wantHTTP2Disabled: true,
+		},
+		{
+			name:    "invalid cipher suite returns error",
+			ciphers: "NOT_A_REAL_CIPHER",
+			wantErr: true,
+		},
+		{
+			name:                 "valid curve preferences are applied",
+			curves:               []string{"X25519", "CurveP256"},
+			wantCurvePreferences: true,
+			wantHTTP2Disabled:    true,
+		},
+		{
+			name:    "invalid curve preference returns error",
+			curves:  []string{"not-a-curve"},
+			wantErr: true,
+		},
+		{
+			name:    "invalid minVersion returns error",
+			minVer:  "1.4",
+			wantErr: true,
+		},
+		{
+			name:  "HTTP/2 enabled does not add disableHTTP2",
+			http2: true,
+		},
+		{
+			name:                 "all settings together",
+			ciphers:              "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+			minVer:               "1.2",
+			curves:               []string{"X25519"},
+			http2:                false,
+			wantMinVersion:       tls.VersionTLS12,
+			wantCipherSuites:     true,
+			wantCurvePreferences: true,
+			wantHTTP2Disabled:    true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			opts, err := buildTLSConfigFuncs(tt.ciphers, tt.minVer, tt.curves, tt.http2)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			cfg := applyTLSOpts(opts)
+
+			require.Equal(t, tt.wantMinVersion, cfg.MinVersion,
+				"MinVersion mismatch")
+
+			if tt.wantCipherSuites {
+				require.NotEmpty(t, cfg.CipherSuites,
+					"expected CipherSuites to be set")
+			} else {
+				require.Empty(t, cfg.CipherSuites,
+					"expected CipherSuites to be empty")
+			}
+
+			if tt.wantCurvePreferences {
+				require.NotEmpty(t, cfg.CurvePreferences,
+					"expected CurvePreferences to be set")
+			} else {
+				require.Empty(t, cfg.CurvePreferences,
+					"expected CurvePreferences to be empty")
+			}
+
+			if tt.wantHTTP2Disabled {
+				require.Equal(t, []string{"http/1.1"}, cfg.NextProtos,
+					"expected HTTP/2 to be disabled")
+			} else {
+				require.Empty(t, cfg.NextProtos,
+					"expected NextProtos to be empty (HTTP/2 enabled)")
+			}
+		})
+	}
+}

+ 19 - 36
cmd/controller/webhook.go

@@ -98,33 +98,12 @@ var webhookCmd = &cobra.Command{
 			}
 		}(c, dnsName, certCheckInterval)
 
-		cipherList, err := getTLSCipherSuitesIDs(tlsCiphers)
+		webhookTLSOpts, err := buildTLSConfigFuncs(tlsCiphers, tlsMinVersion, tlsCurvePreferences, enableHTTP2)
 		if err != nil {
-			ctrl.Log.Error(err, "unable to fetch tls ciphers")
+			setupLog.Error(err, "unable to configure TLS for webhook server")
 			os.Exit(1)
 		}
 
-		// Configure TLS options for webhook server
-		var webhookTLSOpts []func(*tls.Config)
-
-		// Add cipher configuration if needed
-		if len(cipherList) > 0 {
-			webhookTLSOpts = append(webhookTLSOpts, func(cfg *tls.Config) {
-				cfg.CipherSuites = cipherList
-			})
-		}
-
-		// Add TLS version configuration
-		webhookTLSOpts = append(webhookTLSOpts, func(c *tls.Config) {
-			c.MinVersion = tlsVersion(tlsMinVersion)
-		})
-
-		// Add HTTP/2 disabling if needed
-		if !enableHTTP2 {
-			webhookTLSOpts = append(webhookTLSOpts, disableHTTP2)
-		}
-
-		// Configure metrics server options
 		metricsServerOpts := server.Options{
 			BindAddress: metricsAddr,
 		}
@@ -144,10 +123,10 @@ var webhookCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		// Configure TLS options for metrics server
-		var metricsTLSOpts []func(*tls.Config)
-		if !enableHTTP2 {
-			metricsTLSOpts = append(metricsTLSOpts, disableHTTP2)
+		metricsTLSOpts, err := buildTLSConfigFuncs(tlsCiphers, tlsMinVersion, tlsCurvePreferences, enableHTTP2)
+		if err != nil {
+			setupLog.Error(err, "unable to configure TLS for webhook metrics server")
+			os.Exit(1)
 		}
 		metricsServerOpts.TLSOpts = metricsTLSOpts
 
@@ -200,20 +179,19 @@ var webhookCmd = &cobra.Command{
 
 // tlsVersion converts from human-readable TLS version (for example "1.1")
 // to the values accepted by tls.Config (for example 0x301).
-func tlsVersion(version string) uint16 {
+// Returns an error for unrecognized version strings.
+func tlsVersion(version string) (uint16, error) {
 	switch version {
-	case "":
-		return tls.VersionTLS10
 	case "1.0":
-		return tls.VersionTLS10
+		return tls.VersionTLS10, nil
 	case "1.1":
-		return tls.VersionTLS11
+		return tls.VersionTLS11, nil
 	case "1.2":
-		return tls.VersionTLS12
+		return tls.VersionTLS12, nil
 	case "1.3":
-		return tls.VersionTLS13
+		return tls.VersionTLS13, nil
 	default:
-		return tls.VersionTLS13
+		return 0, fmt.Errorf("unsupported TLS minimum version %q; valid values are 1.0, 1.1, 1.2, 1.3", version)
 	}
 }
 
@@ -282,7 +260,12 @@ func init() {
 		" The order of this list does not give preference to the ciphers, the ordering is done automatically."+
 		" Full lists of available ciphers can be found at https://pkg.go.dev/crypto/tls#pkg-constants."+
 		" E.g. 'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256'")
-	webhookCmd.Flags().StringVar(&tlsMinVersion, "tls-min-version", "1.2", "minimum version of TLS supported.")
+	webhookCmd.Flags().StringVar(&tlsMinVersion, "tls-min-version", "", "minimum version of TLS supported. "+
+		"If not specified, Go's default minimum version is used. Valid values: 1.0, 1.1, 1.2, 1.3")
+	webhookCmd.Flags().StringSliceVar(&tlsCurvePreferences, "tls-curve-preferences", nil,
+		"ordered list of TLS key exchange curves for the webhook and metrics servers "+
+			"(for example X25519,CurveP256, or decimal tls.CurveID values supported by this Go toolchain). "+
+			"If omitted, Go defaults are used.")
 	webhookCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false,
 		"If set, HTTP/2 will be enabled for the metrics and webhook server")
 }

+ 12 - 0
deploy/charts/external-secrets/README.md

@@ -113,6 +113,10 @@ The command removes all the Kubernetes components associated with the chart and
 | certController.startupProbe.initialDelaySeconds | int | `10` | Number of seconds after the container has started before the startup probe is initiated. |
 | certController.startupProbe.periodSeconds | int | `10` | How often (in seconds) to perform the startup probe. |
 | certController.strategy | object | `{}` | Set deployment strategy |
+| certController.tls | object | `{"ciphers":"","curvePreferences":[],"minVersion":""}` | CertController-specific TLS security profile overrides. When set, these override the global tls.* values for the cert-controller deployment. |
+| certController.tls.ciphers | string | `""` | Comma-separated list of TLS cipher suites. If empty, the global tls.ciphers is used. +docs:property |
+| certController.tls.curvePreferences | list | `[]` | Ordered list of TLS key exchange curves. If empty, the global tls.curvePreferences is used. +docs:property |
+| certController.tls.minVersion | string | `""` | Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the global tls.minVersion is used. +docs:property |
 | certController.tolerations | list | `[]` |  |
 | certController.topologySpreadConstraints | list | `[]` |  |
 | commonLabels | object | `{}` | Additional labels added to all helm chart resources. |
@@ -264,6 +268,10 @@ The command removes all the Kubernetes components associated with the chart and
 | storeRequeueInterval | string | `""` | Default time duration between reconciling (Cluster)SecretStores. |
 | strategy | object | `{}` | Set deployment strategy |
 | systemAuthDelegator | bool | `false` | If true the system:auth-delegator ClusterRole will be added to RBAC |
+| tls | object | `{"ciphers":"","curvePreferences":[],"minVersion":""}` | TLS security profile settings applied to all controller, webhook, and certController deployments. These can be overridden per-component via webhook.tls and certController.tls. |
+| tls.ciphers | string | `""` | Comma-separated list of TLS cipher suites (TLS_CIPHER_SUITE names). Does not apply to TLS 1.3. If empty, Go defaults apply. +docs:property |
+| tls.curvePreferences | list | `[]` | Ordered list of TLS key exchange curves (e.g. X25519, CurveP256, or decimal CurveID). If empty, Go defaults apply. +docs:property |
+| tls.minVersion | string | `""` | Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the Go CLI default applies. +docs:property |
 | tolerations | list | `[]` |  |
 | topologySpreadConstraints | list | `[]` |  |
 | vault | object | `{"enableTokenCache":false,"tokenCacheSize":262144}` | Vault token cache configuration |
@@ -365,5 +373,9 @@ The command removes all the Kubernetes components associated with the chart and
 | webhook.startupProbe.initialDelaySeconds | int | `10` | Number of seconds after the container has started before the startup probe is initiated. |
 | webhook.startupProbe.periodSeconds | int | `10` | How often (in seconds) to perform the startup probe. |
 | webhook.strategy | object | `{}` | Set deployment strategy |
+| webhook.tls | object | `{"ciphers":"","curvePreferences":[],"minVersion":""}` | Webhook-specific TLS security profile overrides. When set, these override the global tls.* values for the webhook deployment. |
+| webhook.tls.ciphers | string | `""` | Comma-separated list of TLS cipher suites. If empty, the global tls.ciphers is used. +docs:property |
+| webhook.tls.curvePreferences | list | `[]` | Ordered list of TLS key exchange curves. If empty, the global tls.curvePreferences is used. +docs:property |
+| webhook.tls.minVersion | string | `""` | Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the global tls.minVersion is used. +docs:property |
 | webhook.tolerations | list | `[]` |  |
 | webhook.topologySpreadConstraints | list | `[]` |  |

+ 9 - 0
deploy/charts/external-secrets/templates/cert-controller-deployment.yaml

@@ -89,6 +89,15 @@ spec:
           {{- if .Values.enableHTTP2 }}
           - --enable-http2=true
           {{- end }}
+          {{- if (.Values.certController.tls.minVersion | default .Values.tls.minVersion) }}
+          - --tls-min-version={{ .Values.certController.tls.minVersion | default .Values.tls.minVersion }}
+          {{- end }}
+          {{- if (.Values.certController.tls.ciphers | default .Values.tls.ciphers) }}
+          - --tls-ciphers={{ .Values.certController.tls.ciphers | default .Values.tls.ciphers }}
+          {{- end }}
+          {{- if (.Values.certController.tls.curvePreferences | default .Values.tls.curvePreferences) }}
+          - --tls-curve-preferences={{ join "," (.Values.certController.tls.curvePreferences | default .Values.tls.curvePreferences) }}
+          {{- end }}
           {{- if .Values.leaderElect }}
           - --enable-leader-election=true
           {{- end }}

+ 9 - 0
deploy/charts/external-secrets/templates/deployment.yaml

@@ -121,6 +121,15 @@ spec:
           {{- if .Values.enableHTTP2 }}
           - --enable-http2=true
           {{- end }}
+          {{- if .Values.tls.minVersion }}
+          - --tls-min-version={{ .Values.tls.minVersion }}
+          {{- end }}
+          {{- if .Values.tls.ciphers }}
+          - --tls-ciphers={{ .Values.tls.ciphers }}
+          {{- end }}
+          {{- if .Values.tls.curvePreferences }}
+          - --tls-curve-preferences={{ join "," .Values.tls.curvePreferences }}
+          {{- end }}
           {{- if .Values.vault.enableTokenCache }}
           - --enable-vault-token-cache=true
           {{- end }}

+ 9 - 0
deploy/charts/external-secrets/templates/webhook-deployment.yaml

@@ -84,6 +84,15 @@ spec:
           {{- if .Values.enableHTTP2 }}
           - --enable-http2=true
           {{- end }}
+          {{- if (.Values.webhook.tls.minVersion | default .Values.tls.minVersion) }}
+          - --tls-min-version={{ .Values.webhook.tls.minVersion | default .Values.tls.minVersion }}
+          {{- end }}
+          {{- if (.Values.webhook.tls.ciphers | default .Values.tls.ciphers) }}
+          - --tls-ciphers={{ .Values.webhook.tls.ciphers | default .Values.tls.ciphers }}
+          {{- end }}
+          {{- if (.Values.webhook.tls.curvePreferences | default .Values.tls.curvePreferences) }}
+          - --tls-curve-preferences={{ join "," (.Values.webhook.tls.curvePreferences | default .Values.tls.curvePreferences) }}
+          {{- end }}
           {{- range $key, $value := .Values.webhook.extraArgs }}
             {{- if $value }}
           - --{{ $key }}={{ $value }}

+ 42 - 0
deploy/charts/external-secrets/values.schema.json

@@ -380,6 +380,20 @@
                 "strategy": {
                     "type": "object"
                 },
+                "tls": {
+                    "type": "object",
+                    "properties": {
+                        "ciphers": {
+                            "type": "string"
+                        },
+                        "curvePreferences": {
+                            "type": "array"
+                        },
+                        "minVersion": {
+                            "type": "string"
+                        }
+                    }
+                },
                 "tolerations": {
                     "type": "array"
                 },
@@ -1023,6 +1037,20 @@
         "systemAuthDelegator": {
             "type": "boolean"
         },
+        "tls": {
+            "type": "object",
+            "properties": {
+                "ciphers": {
+                    "type": "string"
+                },
+                "curvePreferences": {
+                    "type": "array"
+                },
+                "minVersion": {
+                    "type": "string"
+                }
+            }
+        },
         "tolerations": {
             "type": "array"
         },
@@ -1482,6 +1510,20 @@
                 "strategy": {
                     "type": "object"
                 },
+                "tls": {
+                    "type": "object",
+                    "properties": {
+                        "ciphers": {
+                            "type": "string"
+                        },
+                        "curvePreferences": {
+                            "type": "array"
+                        },
+                        "minVersion": {
+                            "type": "string"
+                        }
+                    }
+                },
                 "tolerations": {
                     "type": "array"
                 },

+ 48 - 0
deploy/charts/external-secrets/values.yaml

@@ -181,6 +181,23 @@ createOperator: true
 # -- if true, HTTP2 will be enabled for the services created by all controllers, curently metrics and webhook.
 enableHTTP2: false
 
+# -- TLS security profile settings applied to all controller, webhook, and certController deployments.
+# These can be overridden per-component via webhook.tls and certController.tls.
+tls:
+  # -- Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the Go CLI default applies.
+  # +docs:property
+  minVersion: ""
+
+  # -- Comma-separated list of TLS cipher suites (TLS_CIPHER_SUITE names).
+  # Does not apply to TLS 1.3. If empty, Go defaults apply.
+  # +docs:property
+  ciphers: ""
+
+  # -- Ordered list of TLS key exchange curves (e.g. X25519, CurveP256, or decimal CurveID).
+  # If empty, Go defaults apply.
+  # +docs:property
+  curvePreferences: []
+
 # -- Vault token cache configuration
 vault:
   # -- Enable Vault token cache. External secrets will reuse the Vault token without creating a new one on each request.
@@ -506,6 +523,22 @@ webhook:
   revisionHistoryLimit: 10
 
   certDir: /tmp/certs
+
+  # -- Webhook-specific TLS security profile overrides.
+  # When set, these override the global tls.* values for the webhook deployment.
+  tls:
+    # -- Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the global tls.minVersion is used.
+    # +docs:property
+    minVersion: ""
+
+    # -- Comma-separated list of TLS cipher suites. If empty, the global tls.ciphers is used.
+    # +docs:property
+    ciphers: ""
+
+    # -- Ordered list of TLS key exchange curves. If empty, the global tls.curvePreferences is used.
+    # +docs:property
+    curvePreferences: []
+
   # -- Specifies whether validating webhooks should be created with failurePolicy: Fail or Ignore
   failurePolicy: Fail
   # -- Specifies if webhook pod should use hostNetwork or not.
@@ -776,6 +809,21 @@ certController:
   # -- Specifies the amount of historic ReplicaSets k8s should keep (see https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy)
   revisionHistoryLimit: 10
 
+  # -- CertController-specific TLS security profile overrides.
+  # When set, these override the global tls.* values for the cert-controller deployment.
+  tls:
+    # -- Minimum TLS version supported (e.g. "1.2" or "1.3"). If empty, the global tls.minVersion is used.
+    # +docs:property
+    minVersion: ""
+
+    # -- Comma-separated list of TLS cipher suites. If empty, the global tls.ciphers is used.
+    # +docs:property
+    ciphers: ""
+
+    # -- Ordered list of TLS key exchange curves. If empty, the global tls.curvePreferences is used.
+    # +docs:property
+    curvePreferences: []
+
   image:
     repository: ghcr.io/external-secrets/external-secrets
     pullPolicy: IfNotPresent

+ 105 - 0
runtime/esutils/tls_curves.go

@@ -0,0 +1,105 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package esutils
+
+import (
+	"crypto/tls"
+	"fmt"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+// ParseCurvePreferences converts curve names to tls.CurveID values, preserving order.
+// An empty slice returns (nil, nil) so tls.Config uses Go defaults.
+//
+// Each entry may be a well-known name matching crypto/tls constants (for example
+// X25519, CurveP256, CurveP384, CurveP521) or a decimal tls.CurveID as supported
+// by the Go toolchain (for example hybrid post-quantum groups when available).
+func ParseCurvePreferences(names []string) ([]tls.CurveID, error) {
+	if len(names) == 0 {
+		return nil, nil
+	}
+	out := make([]tls.CurveID, 0, len(names))
+	for _, raw := range names {
+		name := strings.TrimSpace(raw)
+		if name == "" {
+			return nil, fmt.Errorf("empty curve preference entry")
+		}
+		id, err := parseCurveID(name)
+		if err != nil {
+			return nil, err
+		}
+		out = append(out, id)
+	}
+	return out, nil
+}
+
+var knownCurves = map[tls.CurveID]bool{
+	tls.CurveP256:          true,
+	tls.CurveP384:          true,
+	tls.CurveP521:          true,
+	tls.X25519:             true,
+	tls.X25519MLKEM768:     true,
+	tls.SecP256r1MLKEM768:  true,
+	tls.SecP384r1MLKEM1024: true,
+}
+
+func parseCurveID(name string) (tls.CurveID, error) {
+	if isAllDecimal(name) {
+		u, err := strconv.ParseUint(name, 10, 16)
+		if err != nil {
+			return 0, fmt.Errorf("invalid tls curve id %q: %w", name, err)
+		}
+		id := tls.CurveID(u)
+		if !knownCurves[id] {
+			return 0, fmt.Errorf(
+				"unsupported tls curve id %s: not a known CurveID"+
+					" (valid: 23=CurveP256, 24=CurveP384, 25=CurveP521,"+
+					" 29=X25519, 4587=SecP256r1MLKEM768,"+
+					" 4588=X25519MLKEM768, 4589=SecP384r1MLKEM1024)",
+				name,
+			)
+		}
+		return id, nil
+	}
+
+	switch name {
+	case "X25519":
+		return tls.X25519, nil
+	case "CurveP256", "P-256", "P256":
+		return tls.CurveP256, nil
+	case "CurveP384", "P-384", "P384":
+		return tls.CurveP384, nil
+	case "CurveP521", "P-521", "P521":
+		return tls.CurveP521, nil
+	default:
+		return 0, fmt.Errorf("unknown tls curve preference %q (use a name like X25519 or a decimal CurveID)", name)
+	}
+}
+
+func isAllDecimal(s string) bool {
+	if s == "" {
+		return false
+	}
+	for _, r := range s {
+		if !unicode.IsDigit(r) {
+			return false
+		}
+	}
+	return true
+}

+ 111 - 0
runtime/esutils/tls_curves_test.go

@@ -0,0 +1,111 @@
+/*
+Copyright © The ESO Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package esutils
+
+import (
+	"crypto/tls"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseCurvePreferences(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   []string
+		want    []tls.CurveID
+		wantErr bool
+	}{
+		{
+			name:  "well-known curve names",
+			input: []string{"X25519", "CurveP256"},
+			want:  []tls.CurveID{tls.X25519, tls.CurveP256},
+		},
+		{
+			name:  "curve name aliases",
+			input: []string{"P-256", "P384"},
+			want:  []tls.CurveID{tls.CurveP256, tls.CurveP384},
+		},
+		{
+			name:  "whitespace is trimmed",
+			input: []string{" X25519 ", "CurveP256"},
+			want:  []tls.CurveID{tls.X25519, tls.CurveP256},
+		},
+		{
+			name:  "nil input uses Go defaults",
+			input: nil,
+			want:  nil,
+		},
+		{
+			name:  "empty input uses Go defaults",
+			input: []string{},
+			want:  nil,
+		},
+		{
+			name:    "unknown curve name",
+			input:   []string{"not-a-curve"},
+			wantErr: true,
+		},
+		{
+			name:    "empty curve entry",
+			input:   []string{"X25519", ""},
+			wantErr: true,
+		},
+		{
+			name:  "decimal curve ID",
+			input: []string{"29"},
+			want:  []tls.CurveID{tls.X25519},
+		},
+		{
+			name:  "all four standard curves",
+			input: []string{"X25519", "CurveP256", "CurveP384", "CurveP521"},
+			want:  []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384, tls.CurveP521},
+		},
+		{
+			name:  "P-521 alias",
+			input: []string{"P-521"},
+			want:  []tls.CurveID{tls.CurveP521},
+		},
+		{
+			name:    "invalid numeric curve ID",
+			input:   []string{"9999"},
+			wantErr: true,
+		},
+		{
+			name:    "zero is not a valid curve ID",
+			input:   []string{"0"},
+			wantErr: true,
+		},
+		{
+			name:  "valid post-quantum hybrid curve by number",
+			input: []string{"4588"},
+			want:  []tls.CurveID{tls.X25519MLKEM768},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ParseCurvePreferences(tt.input)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}