Skip to content

Commit 3fa7077

Browse files
Merge pull request #1938 from marbindrakon/allow-acme-public-certs
Allow ACME-issued certificates for public endpoint routes
2 parents 38a9f8b + eb23f35 commit 3fa7077

2 files changed

Lines changed: 95 additions & 9 deletions

File tree

internal/openstack/common.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ func (ed *EndpointDetail) CreateRoute(
602602
certSecret := &k8s_corev1.Secret{}
603603

604604
// if a custom cert secret was provided, check if it exist
605-
// and has the required cert, key and cacert
605+
// and has the required cert and key (cacert optional)
606606
// Right now there is no check if certificate is valid for
607607
// the hostname of the route. If the referenced secret is
608608
// there and has the required files it is just being used.
@@ -616,13 +616,9 @@ func (ed *EndpointDetail) CreateRoute(
616616
return ctrl.Result{}, err
617617
}
618618

619-
// check if secret has the expected entries tls.crt, tls.key and ca.crt
620-
if certSecret != nil {
621-
for _, key := range []string{"tls.crt", "tls.key", "ca.crt"} {
622-
if _, exist := certSecret.Data[key]; !exist {
623-
return ctrl.Result{}, fmt.Errorf("certificate secret %s does not provide %s", *ed.Route.TLS.SecretName, key)
624-
}
625-
}
619+
// check the secret has the required tls.crt and tls.key entries
620+
if err := validateRouteCertSecret(certSecret, *ed.Route.TLS.SecretName); err != nil {
621+
return ctrl.Result{}, err
626622
}
627623
}
628624

@@ -659,9 +655,13 @@ func (ed *EndpointDetail) CreateRoute(
659655
Termination: routev1.TLSTerminationEdge,
660656
Certificate: string(certSecret.Data[tls.CertKey]),
661657
Key: string(certSecret.Data[tls.PrivateKey]),
662-
CACertificate: string(certSecret.Data[tls.CAKey]),
663658
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
664659
}
660+
// ca.crt is optional (absent for ACME-issued certs); only set the
661+
// route CACertificate when the secret actually provides it.
662+
if caCert, ok := certSecret.Data[tls.CAKey]; ok {
663+
tlsConfig.CACertificate = string(caCert)
664+
}
665665

666666
// for internal TLS (TLSE) use routev1.TLSTerminationReencrypt
667667
if ed.Service.TLS.Enabled && (ed.Service.TLS.SecretName != nil || hasCertInOverrideSpec(ed.Route.OverrideSpec)) {
@@ -872,6 +872,23 @@ func hasCertInOverrideSpec(overrideSpec route.OverrideSpec) bool {
872872
overrideSpec.Spec.TLS.Key != ""
873873
}
874874

875+
// validateRouteCertSecret ensures a user-provided route TLS secret contains the
876+
// required tls.crt and tls.key entries. ca.crt is intentionally not required:
877+
// certificates issued by an ACME issuer (e.g. Let's Encrypt) do not populate
878+
// ca.crt, and the issuing chain is delivered via tls.crt instead. ca.crt is only
879+
// needed to advertise a custom CA on the route, which is optional.
880+
func validateRouteCertSecret(certSecret *k8s_corev1.Secret, secretName string) error {
881+
if certSecret == nil {
882+
return nil
883+
}
884+
for _, key := range []string{tls.CertKey, tls.PrivateKey} {
885+
if _, exist := certSecret.Data[key]; !exist {
886+
return fmt.Errorf("certificate secret %s does not provide %s", secretName, key)
887+
}
888+
}
889+
return nil
890+
}
891+
875892
func serviceExists(route string, services *k8s_corev1.ServiceList) bool {
876893
for _, svc := range services.Items {
877894
if svc.Name == route {

internal/openstack/common_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,72 @@ func TestCheckRouteAdmissionStatus_UpdatedStatus(t *testing.T) {
572572
err = checkRouteAdmissionStatus(ctx, h, "test-route", "test-namespace")
573573
g.Expect(err).ToNot(HaveOccurred())
574574
}
575+
576+
func TestValidateRouteCertSecret(t *testing.T) {
577+
tests := []struct {
578+
name string
579+
data map[string][]byte
580+
wantErr bool
581+
errSubstr string
582+
}{
583+
{
584+
name: "cert, key and ca.crt present",
585+
data: map[string][]byte{
586+
"tls.crt": []byte("cert"),
587+
"tls.key": []byte("key"),
588+
"ca.crt": []byte("ca"),
589+
},
590+
wantErr: false,
591+
},
592+
{
593+
// ACME issuers (e.g. Let's Encrypt) do not populate ca.crt; the
594+
// secret must still be accepted.
595+
name: "cert and key present, ca.crt absent (ACME)",
596+
data: map[string][]byte{
597+
"tls.crt": []byte("cert"),
598+
"tls.key": []byte("key"),
599+
},
600+
wantErr: false,
601+
},
602+
{
603+
name: "tls.key missing",
604+
data: map[string][]byte{
605+
"tls.crt": []byte("cert"),
606+
},
607+
wantErr: true,
608+
errSubstr: "does not provide tls.key",
609+
},
610+
{
611+
name: "tls.crt missing",
612+
data: map[string][]byte{
613+
"tls.key": []byte("key"),
614+
},
615+
wantErr: true,
616+
errSubstr: "does not provide tls.crt",
617+
},
618+
}
619+
620+
for _, tt := range tests {
621+
t.Run(tt.name, func(t *testing.T) {
622+
g := NewWithT(t)
623+
secret := &k8s_corev1.Secret{
624+
ObjectMeta: metav1.ObjectMeta{Name: "custom-route-cert", Namespace: "test-namespace"},
625+
Data: tt.data,
626+
}
627+
628+
err := validateRouteCertSecret(secret, secret.Name)
629+
if tt.wantErr {
630+
g.Expect(err).To(HaveOccurred())
631+
g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr))
632+
} else {
633+
g.Expect(err).ToNot(HaveOccurred())
634+
}
635+
})
636+
}
637+
}
638+
639+
func TestValidateRouteCertSecret_NilSecret(t *testing.T) {
640+
g := NewWithT(t)
641+
// A nil secret is tolerated (the caller may not have fetched one).
642+
g.Expect(validateRouteCertSecret(nil, "custom-route-cert")).ToNot(HaveOccurred())
643+
}

0 commit comments

Comments
 (0)