From 26c7fc3a3d30ddf32e9e6526db5899129126a441 Mon Sep 17 00:00:00 2001 From: Vimal Solanki Date: Mon, 11 May 2026 20:00:25 +0530 Subject: [PATCH] fix(cpo/pki): add etcd-client SANs to peer certificate Both etcd-discovery and etcd-client headless services select the same etcd pods, causing CoreDNS to register two PTR records per pod IP. The etcd peer certificate only included *.etcd-discovery SANs, so when Go's getnameinfo() non-deterministically returned the etcd-client PTR record, the TLS SAN check failed and peer connections were rejected. Add *.etcd-client wildcard SANs to the peer certificate so both possible PTR lookup results match. --- .../hostedcontrolplane/pki/etcd.go | 2 + .../hostedcontrolplane/pki/etcd_test.go | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go diff --git a/control-plane-operator/controllers/hostedcontrolplane/pki/etcd.go b/control-plane-operator/controllers/hostedcontrolplane/pki/etcd.go index 69c6526d7b6..9ffbc61b8d6 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/pki/etcd.go +++ b/control-plane-operator/controllers/hostedcontrolplane/pki/etcd.go @@ -45,6 +45,8 @@ func ReconcileEtcdPeerSecret(secret, ca *corev1.Secret, ownerRef config.OwnerRef dnsNames := []string{ fmt.Sprintf("*.etcd-discovery.%s.svc", secret.Namespace), fmt.Sprintf("*.etcd-discovery.%s.svc.cluster.local", secret.Namespace), + fmt.Sprintf("*.etcd-client.%s.svc", secret.Namespace), + fmt.Sprintf("*.etcd-client.%s.svc.cluster.local", secret.Namespace), "127.0.0.1", "::1", } diff --git a/control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go b/control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go new file mode 100644 index 00000000000..4de2eb403df --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go @@ -0,0 +1,81 @@ +package pki + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" + + . "github.com/onsi/gomega" + + "github.com/openshift/hypershift/support/certs" + "github.com/openshift/hypershift/support/config" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestReconcileEtcdPeerSecret(t *testing.T) { + t.Parallel() + + caCfg := certs.CertCfg{ + IsCA: true, + Subject: pkix.Name{CommonName: "etcd-signer", OrganizationalUnit: []string{"openshift"}}, + } + caKey, caCert, err := certs.GenerateSelfSignedCertificate(&caCfg) + if err != nil { + t.Fatalf("failed to generate CA: %v", err) + } + caSecret := &corev1.Secret{ + Data: map[string][]byte{ + certs.CASignerCertMapKey: certs.CertToPem(caCert), + certs.CASignerKeyMapKey: certs.PrivateKeyToPem(caKey), + }, + } + + t.Run("When reconciling etcd peer secret it should include both etcd-discovery and etcd-client SANs", func(t *testing.T) { + g := NewWithT(t) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "clusters-test", + }, + } + + err := ReconcileEtcdPeerSecret(secret, caSecret, config.OwnerRef{}) + g.Expect(err).ToNot(HaveOccurred()) + + certData := secret.Data[EtcdPeerCrtKey] + g.Expect(certData).ToNot(BeEmpty()) + + cert, err := certs.PemToCertificate(certData) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(cert.DNSNames).To(ContainElements( + "*.etcd-discovery.clusters-test.svc", + "*.etcd-discovery.clusters-test.svc.cluster.local", + "*.etcd-client.clusters-test.svc", + "*.etcd-client.clusters-test.svc.cluster.local", + "127.0.0.1", + "::1", + )) + }) + + t.Run("When reconciling etcd peer secret it should have client and server auth usage", func(t *testing.T) { + g := NewWithT(t) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "clusters-test", + }, + } + + err := ReconcileEtcdPeerSecret(secret, caSecret, config.OwnerRef{}) + g.Expect(err).ToNot(HaveOccurred()) + + cert, err := certs.PemToCertificate(secret.Data[EtcdPeerCrtKey]) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(cert.ExtKeyUsage).To(ContainElements( + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + )) + }) +}