Skip to content

Commit b2d30a8

Browse files
committed
feat: classic elb fix for TLS issues
There is an issue when creating clusters (or upgrading clusters) with kubernetes versions v1.30+ and using a classic elb with an SSL health check (which the default for new clusters). The problem is that Kubernetes v1.30+ switched to Go 1.22 which removed the RSA ciphers. This then causes the ELB health check to fail. There are a number of workarounds including manually specifying the cipher suites to use for the api server. This commit does the following: - Adds warnings to the AWSCluster webhook to alert users that: - their cluster is using a classic elb and this is now deprecated - their cluster is using the default health check protocol which warnings previously SSL and that now the default has changed to TCP. - Will update the health check to TCP if the load balancer is "classic" and the health check protocol is not set. Signed-off-by: Richard Case <[email protected]>
1 parent 8cc4beb commit b2d30a8

20 files changed

+734
-68
lines changed

api/v1beta2/awscluster_webhook.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ import (
3333
"sigs.k8s.io/cluster-api/util/annotations"
3434
)
3535

36+
const (
37+
warningClassicELB = "%s load balancer is using a classic elb which is deprecated & support will be removed in a future release, please consider using another type of load balancer instead"
38+
warningHealthCheckProtocolNotSet = "healthcheck protocol is not set, the default value has changed from SSL to TCP. Health checks for existing clusters will be updated to TCP"
39+
)
40+
3641
// log is for logging in this package.
3742
var _ = ctrl.Log.WithName("awscluster-resource")
3843

@@ -53,15 +58,23 @@ var (
5358
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
5459
func (r *AWSCluster) ValidateCreate() (admission.Warnings, error) {
5560
var allErrs field.ErrorList
61+
var allWarnings admission.Warnings
5662

5763
allErrs = append(allErrs, r.Spec.Bastion.Validate()...)
5864
allErrs = append(allErrs, r.validateSSHKeyName()...)
5965
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
6066
allErrs = append(allErrs, r.Spec.S3Bucket.Validate()...)
6167
allErrs = append(allErrs, r.validateNetwork()...)
62-
allErrs = append(allErrs, r.validateControlPlaneLBs()...)
6368

64-
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
69+
warnings, errs := r.validateControlPlaneLBs()
70+
if len(errs) > 0 {
71+
allErrs = append(allErrs, errs...)
72+
}
73+
if len(warnings) > 0 {
74+
allWarnings = append(allWarnings, warnings...)
75+
}
76+
77+
return allWarnings, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
6578
}
6679

6780
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
@@ -72,6 +85,7 @@ func (r *AWSCluster) ValidateDelete() (admission.Warnings, error) {
7285
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
7386
func (r *AWSCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
7487
var allErrs field.ErrorList
88+
var allWarnings admission.Warnings
7589

7690
allErrs = append(allErrs, r.validateGCTasksAnnotation()...)
7791

@@ -139,7 +153,23 @@ func (r *AWSCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, err
139153
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
140154
allErrs = append(allErrs, r.Spec.S3Bucket.Validate()...)
141155

142-
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
156+
if r.Spec.ControlPlaneLoadBalancer != nil {
157+
if r.Spec.ControlPlaneLoadBalancer.LoadBalancerType == LoadBalancerTypeClassic {
158+
allWarnings = append(allWarnings, fmt.Sprintf(warningClassicELB, "primary control plane"))
159+
}
160+
}
161+
162+
if r.Spec.SecondaryControlPlaneLoadBalancer != nil {
163+
if r.Spec.SecondaryControlPlaneLoadBalancer.LoadBalancerType == LoadBalancerTypeClassic {
164+
allWarnings = append(allWarnings, fmt.Sprintf(warningClassicELB, "secondary control plane"))
165+
}
166+
}
167+
168+
if r.Spec.ControlPlaneLoadBalancer == nil || r.Spec.ControlPlaneLoadBalancer.HealthCheckProtocol == nil {
169+
allWarnings = append(allWarnings, fmt.Sprintf("%s. Existing load balancers will be updates", warningHealthCheckProtocolNotSet))
170+
}
171+
172+
return allWarnings, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
143173
}
144174

145175
func (r *AWSCluster) validateControlPlaneLoadBalancerUpdate(oldlb, newlb *AWSLoadBalancerSpec) field.ErrorList {
@@ -184,11 +214,13 @@ func (r *AWSCluster) validateControlPlaneLoadBalancerUpdate(oldlb, newlb *AWSLoa
184214
// Block the update for Protocol :
185215
// - if it was not set in old spec but added in new spec
186216
// - if it was set in old spec but changed in new spec
187-
if !cmp.Equal(newlb.HealthCheckProtocol, oldlb.HealthCheckProtocol) {
188-
allErrs = append(allErrs,
189-
field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "healthCheckProtocol"),
190-
newlb.HealthCheckProtocol, "field is immutable once set"),
191-
)
217+
if oldlb.LoadBalancerType != LoadBalancerTypeClassic {
218+
if !cmp.Equal(newlb.HealthCheckProtocol, oldlb.HealthCheckProtocol) {
219+
allErrs = append(allErrs,
220+
field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "healthCheckProtocol"),
221+
newlb.HealthCheckProtocol, "field is immutable once set"),
222+
)
223+
}
192224
}
193225
}
194226

@@ -301,8 +333,21 @@ func (r *AWSCluster) validateNetwork() field.ErrorList {
301333
return allErrs
302334
}
303335

304-
func (r *AWSCluster) validateControlPlaneLBs() field.ErrorList {
336+
func (r *AWSCluster) validateControlPlaneLBs() (admission.Warnings, field.ErrorList) {
305337
var allErrs field.ErrorList
338+
var allWarnings admission.Warnings
339+
340+
if r.Spec.ControlPlaneLoadBalancer != nil && r.Spec.ControlPlaneLoadBalancer.LoadBalancerType == LoadBalancerTypeClassic {
341+
allWarnings = append(allWarnings, fmt.Sprintf(warningClassicELB, "primary control plane"))
342+
343+
if r.Spec.ControlPlaneLoadBalancer.HealthCheckProtocol == nil {
344+
allWarnings = append(allWarnings, warningHealthCheckProtocolNotSet)
345+
}
346+
347+
if r.Spec.ControlPlaneLoadBalancer.HealthCheckProtocol != nil && *r.Spec.ControlPlaneLoadBalancer.HealthCheckProtocol == ELBProtocolSSL {
348+
allWarnings = append(allWarnings, "loadbalancer is using a classic elb with SSL health check, this causes issues with ciper suites with kubernetes v1.30+")
349+
}
350+
}
306351

307352
// If the secondary is defined, check that the name is not empty and different from the primary.
308353
// Also, ensure that the secondary load balancer is an NLB
@@ -322,6 +367,9 @@ func (r *AWSCluster) validateControlPlaneLBs() field.ErrorList {
322367
if r.Spec.SecondaryControlPlaneLoadBalancer.LoadBalancerType != LoadBalancerTypeNLB {
323368
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "secondaryControlPlaneLoadBalancer", "loadBalancerType"), r.Spec.SecondaryControlPlaneLoadBalancer.LoadBalancerType, "secondary control plane load balancer must be a Network Load Balancer"))
324369
}
370+
if r.Spec.SecondaryControlPlaneLoadBalancer.LoadBalancerType == LoadBalancerTypeClassic {
371+
allWarnings = append(allWarnings, fmt.Sprintf(warningClassicELB, "secondary control plane"))
372+
}
325373
}
326374

327375
// Additional listeners are only supported for NLBs.
@@ -378,7 +426,7 @@ func (r *AWSCluster) validateControlPlaneLBs() field.ErrorList {
378426
}
379427
}
380428

381-
return allErrs
429+
return allWarnings, allErrs
382430
}
383431

384432
func (r *AWSCluster) validateIngressRule(rule IngressRule) field.ErrorList {

api/v1beta2/awscluster_webhook_test.go

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ import (
3232

3333
"sigs.k8s.io/cluster-api-provider-aws/v2/feature"
3434
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
35-
utildefaulting "sigs.k8s.io/cluster-api/util/defaulting"
35+
"sigs.k8s.io/cluster-api/util/defaulting"
3636
)
3737

3838
func TestAWSClusterDefault(t *testing.T) {
3939
cluster := &AWSCluster{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
40-
t.Run("for AWSCluster", utildefaulting.DefaultValidateTest(cluster))
40+
t.Run("for AWSCluster", defaultValidateTest(cluster, true))
4141
cluster.Default()
4242
g := NewWithT(t)
4343
g.Expect(cluster.Spec.IdentityRef).NotTo(BeNil())
@@ -699,7 +699,7 @@ func TestAWSClusterValidateCreate(t *testing.T) {
699699
}
700700

701701
func TestAWSClusterValidateUpdate(t *testing.T) {
702-
var tests = []struct {
702+
tests := []struct {
703703
name string
704704
oldCluster *AWSCluster
705705
newCluster *AWSCluster
@@ -967,23 +967,45 @@ func TestAWSClusterValidateUpdate(t *testing.T) {
967967
wantErr: true,
968968
},
969969
{
970-
name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is updated",
970+
name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is updated and not classic elb",
971971
oldCluster: &AWSCluster{
972972
Spec: AWSClusterSpec{
973973
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
974+
LoadBalancerType: LoadBalancerTypeNLB,
974975
HealthCheckProtocol: &ELBProtocolTCP,
975976
},
976977
},
977978
},
978979
newCluster: &AWSCluster{
979980
Spec: AWSClusterSpec{
980981
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
982+
LoadBalancerType: LoadBalancerTypeNLB,
981983
HealthCheckProtocol: &ELBProtocolSSL,
982984
},
983985
},
984986
},
985987
wantErr: true,
986988
},
989+
{
990+
name: "Should pass if controlPlaneLoadBalancer healthcheckprotocol is updated and a classic elb",
991+
oldCluster: &AWSCluster{
992+
Spec: AWSClusterSpec{
993+
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
994+
LoadBalancerType: LoadBalancerTypeClassic,
995+
HealthCheckProtocol: &ELBProtocolSSL,
996+
},
997+
},
998+
},
999+
newCluster: &AWSCluster{
1000+
Spec: AWSClusterSpec{
1001+
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
1002+
LoadBalancerType: LoadBalancerTypeClassic,
1003+
HealthCheckProtocol: &ELBProtocolTCP,
1004+
},
1005+
},
1006+
},
1007+
wantErr: false,
1008+
},
9871009
{
9881010
name: "Should pass if old secondary lb is absent",
9891011
oldCluster: &AWSCluster{
@@ -1017,19 +1039,43 @@ func TestAWSClusterValidateUpdate(t *testing.T) {
10171039
wantErr: false,
10181040
},
10191041
{
1020-
name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is changed to non-default if it was not set before update",
1042+
name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is changed to non-default if it was not set before update for non-classic elb",
10211043
oldCluster: &AWSCluster{
1022-
Spec: AWSClusterSpec{},
1044+
Spec: AWSClusterSpec{
1045+
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
1046+
LoadBalancerType: LoadBalancerTypeNLB,
1047+
},
1048+
},
10231049
},
10241050
newCluster: &AWSCluster{
10251051
Spec: AWSClusterSpec{
10261052
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
1053+
LoadBalancerType: LoadBalancerTypeNLB,
10271054
HealthCheckProtocol: &ELBProtocolTCP,
10281055
},
10291056
},
10301057
},
10311058
wantErr: true,
10321059
},
1060+
{
1061+
name: "Should pass if controlPlaneLoadBalancer healthcheckprotocol is changed to non-default if it was not set before update for classic elb",
1062+
oldCluster: &AWSCluster{
1063+
Spec: AWSClusterSpec{
1064+
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
1065+
LoadBalancerType: LoadBalancerTypeClassic,
1066+
},
1067+
},
1068+
},
1069+
newCluster: &AWSCluster{
1070+
Spec: AWSClusterSpec{
1071+
ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
1072+
LoadBalancerType: LoadBalancerTypeNLB,
1073+
HealthCheckProtocol: &ELBProtocolTCP,
1074+
},
1075+
},
1076+
},
1077+
wantErr: false,
1078+
},
10331079
{
10341080
name: "correct GC tasks annotation",
10351081
oldCluster: &AWSCluster{
@@ -1357,3 +1403,48 @@ func TestAWSClusterDefaultAllowedCIDRBlocks(t *testing.T) {
13571403
})
13581404
}
13591405
}
1406+
1407+
// defaultValidateTest returns a new testing function to be used in tests to
1408+
// make sure defaulting webhooks also pass validation tests on create,
1409+
// update and delete.
1410+
// NOTE: This is a copy of the DefaultValidateTest function in the cluster-api
1411+
// package, but it has been modified to allow warnings to be returned.
1412+
func defaultValidateTest(object defaulting.DefaultingValidator, allowWarnings bool) func(*testing.T) {
1413+
return func(t *testing.T) {
1414+
t.Helper()
1415+
1416+
createCopy := object.DeepCopyObject().(defaulting.DefaultingValidator)
1417+
updateCopy := object.DeepCopyObject().(defaulting.DefaultingValidator)
1418+
deleteCopy := object.DeepCopyObject().(defaulting.DefaultingValidator)
1419+
defaultingUpdateCopy := updateCopy.DeepCopyObject().(defaulting.DefaultingValidator)
1420+
1421+
t.Run("validate-on-create", func(t *testing.T) {
1422+
g := NewWithT(t)
1423+
createCopy.Default()
1424+
warnings, err := createCopy.ValidateCreate()
1425+
g.Expect(err).ToNot(HaveOccurred())
1426+
if !allowWarnings {
1427+
g.Expect(warnings).To(BeEmpty())
1428+
}
1429+
})
1430+
t.Run("validate-on-update", func(t *testing.T) {
1431+
g := NewWithT(t)
1432+
defaultingUpdateCopy.Default()
1433+
updateCopy.Default()
1434+
warnings, err := defaultingUpdateCopy.ValidateUpdate(updateCopy)
1435+
g.Expect(err).ToNot(HaveOccurred())
1436+
if !allowWarnings {
1437+
g.Expect(warnings).To(BeEmpty())
1438+
}
1439+
})
1440+
t.Run("validate-on-delete", func(t *testing.T) {
1441+
g := NewWithT(t)
1442+
deleteCopy.Default()
1443+
warnings, err := deleteCopy.ValidateDelete()
1444+
g.Expect(err).ToNot(HaveOccurred())
1445+
if !allowWarnings {
1446+
g.Expect(warnings).To(BeEmpty())
1447+
}
1448+
})
1449+
}
1450+
}

0 commit comments

Comments
 (0)