From d4c53e84e8d50f646859fdc4b87a26fb8b655563 Mon Sep 17 00:00:00 2001 From: Chai Bot Date: Tue, 30 Jun 2026 12:10:26 +0000 Subject: [PATCH] Bug OCPBUGS-94106: Fall back to TLSProfileIntermediateType ciphers when observedConfig is empty during bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During bootstrap, the config observation controller hasn't converged yet, so observedConfig.servingInfo.cipherSuites and minTLSVersion are empty. This caused getCipherSuites() and getObservedTLSMinVersion() to hard-error, preventing the etcd static pod from rendering. Fall back to TLSProfileIntermediateType defaults (matching the render path in pkg/cmd/render/env.go:getTLSCipherSuites) when the observed values are empty, and log a warning so the fallback is auditable. The fallback only triggers when the config is genuinely empty — observedConfig with unrecognized ciphers still errors as before. Co-Authored-By: Claude Opus 4.6 --- pkg/etcdenvvar/etcd_env.go | 20 ++++++- pkg/etcdenvvar/etcd_env_test.go | 98 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/pkg/etcdenvvar/etcd_env.go b/pkg/etcdenvvar/etcd_env.go index e226aca3c0..8c4a474052 100644 --- a/pkg/etcdenvvar/etcd_env.go +++ b/pkg/etcdenvvar/etcd_env.go @@ -295,6 +295,14 @@ func getObservedTLSMinVersion(envVarContext envVarContext) (tlsutil.TLSVersion, return "", fmt.Errorf("couldn't get minTLSVersion from observedConfig: %w", err) } + // During bootstrap the config observation controller hasn't converged yet, + // so observedConfig.servingInfo.minTLSVersion will be empty. Fall back to + // TLSProfileIntermediateType defaults (TLS 1.2). + if observedMinTLSVersion == "" { + klog.Warningf("observedConfig minTLSVersion is empty, falling back to TLSProfileIntermediateType default (TLS 1.2)") + return tlsutil.TLSVersion12, nil + } + // map tls version to string recognized by etcd v, err := crypto.TLSVersion(observedMinTLSVersion) if err != nil { @@ -328,10 +336,20 @@ func getCipherSuites(envVarContext envVarContext) (map[string]string, error) { return nil, fmt.Errorf("couldn't get cipherSuites from observedConfig: %w", err) } + // During bootstrap the config observation controller hasn't converged yet, + // so observedConfig.servingInfo.cipherSuites will be empty. Fall back to + // TLSProfileIntermediateType defaults, matching the render path + // (pkg/cmd/render/env.go:getTLSCipherSuites). + if len(observedCipherSuites) == 0 { + klog.Warningf("observedConfig cipherSuites is empty, falling back to TLSProfileIntermediateType defaults") + profileSpec := v1.TLSProfiles[v1.TLSProfileIntermediateType] + observedCipherSuites = crypto.OpenSSLToIANACipherSuites(profileSpec.Ciphers) + } + actualCipherSuites := tlshelpers.SupportedEtcdCiphers(observedCipherSuites) if len(actualCipherSuites) == 0 { - return nil, fmt.Errorf("no supported cipherSuites not found in observedConfig") + return nil, fmt.Errorf("no supported cipherSuites found in observedConfig") } observedMinTLSVersion, err := getObservedTLSMinVersion(envVarContext) diff --git a/pkg/etcdenvvar/etcd_env_test.go b/pkg/etcdenvvar/etcd_env_test.go index a7d28d4c45..8319c25639 100644 --- a/pkg/etcdenvvar/etcd_env_test.go +++ b/pkg/etcdenvvar/etcd_env_test.go @@ -1,11 +1,109 @@ package etcdenvvar import ( + "strings" "testing" + operatorv1 "github.com/openshift/api/operator/v1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/runtime" ) +func TestGetCipherSuites(t *testing.T) { + testCases := []struct { + name string + observedConfig map[string]any + expectErr bool + errContains string + expectEnvKey string + expectCiphers []string + }{ + { + name: "populated observedConfig returns expected ciphers", + observedConfig: map[string]any{ + "servingInfo": map[string]any{ + "cipherSuites": []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + "minTLSVersion": "VersionTLS12", + }, + }, + expectEnvKey: "ETCD_CIPHER_SUITES", + expectCiphers: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + }, + }, + { + name: "empty observedConfig falls back to TLSProfileIntermediateType defaults", + observedConfig: map[string]any{}, + expectEnvKey: "ETCD_CIPHER_SUITES", + expectCiphers: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + }, + }, + { + name: "observedConfig with only unsupported ciphers returns error", + observedConfig: map[string]any{ + "servingInfo": map[string]any{ + "cipherSuites": []string{ + "TLS_UNSUPPORTED_CIPHER_1", + "TLS_UNSUPPORTED_CIPHER_2", + }, + "minTLSVersion": "VersionTLS12", + }, + }, + expectErr: true, + errContains: "no supported cipherSuites found", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + observedConfigYaml, err := yaml.Marshal(tc.observedConfig) + require.NoError(t, err) + + ctx := envVarContext{ + spec: operatorv1.StaticPodOperatorSpec{ + OperatorSpec: operatorv1.OperatorSpec{ + ObservedConfig: runtime.RawExtension{Raw: observedConfigYaml}, + }, + }, + } + + result, err := getCipherSuites(ctx) + + if tc.expectErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errContains) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + + cipherValue, ok := result[tc.expectEnvKey] + require.True(t, ok, "expected key %q in result", tc.expectEnvKey) + + actualCiphers := strings.Split(cipherValue, ",") + assert.Equal(t, tc.expectCiphers, actualCiphers) + }) + } +} + func TestConvertDBSize(t *testing.T) { testCases := []struct { name string