Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Comment on lines +57 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how ReconcileEtcdPeerSecret handles IP addresses in certificate generation

# Search for the implementation of ReconcileEtcdPeerSecret
ast-grep --pattern $'func ReconcileEtcdPeerSecret($$$) $$$'

# Search for certificate generation calls that might include IPs
rg -A 10 -B 5 "127\.0\.0\.1|::1" --type go --glob '**/pki/etcd.go'

# Look for DNSNames and IPAddresses field assignments
rg -A 5 "DNSNames|IPAddresses" --type go --glob '**/pki/etcd.go'

Repository: openshift/hypershift

Length of output: 2726


🏁 Script executed:

# Find the reconcileSignedCertWithKeysAndAddresses function implementation
ast-grep --pattern $'func reconcileSignedCertWithKeysAndAddresses($$$) $$$'

# Also search for where DNSNames is actually set in the certificate
rg -B 5 -A 15 "DNSNames\s*=" --type go --glob '**/pki/*.go' | head -100

# Look for IPAddresses assignment in certificate generation
rg -B 5 -A 5 "IPAddresses\s*=" --type go --glob '**/pki/*.go'

Repository: openshift/hypershift

Length of output: 1123


🏁 Script executed:

# Find where certs.ReconcileSignedCert is imported from
rg "import.*certs" --type go --glob '**/pki/cert.go' -A 20 | head -40

# Find the certs package and ReconcileSignedCert implementation
find . -name "*.go" -type f -exec grep -l "func ReconcileSignedCert" {} \;

# Search for how it populates certificate fields
rg -B 5 -A 20 "func ReconcileSignedCert" --type go

Repository: openshift/hypershift

Length of output: 1324


🏁 Script executed:

# Get the full ReconcileSignedCert implementation
sed -n '1,300p' support/certs/tls.go | tail -200

Repository: openshift/hypershift

Length of output: 6056


🏁 Script executed:

# Find where the CSR is generated with dnsNames and ips in ReconcileSignedCert
sed -n '/^func ReconcileSignedCert/,/^func [A-Z]/p' support/certs/tls.go | head -100

Repository: openshift/hypershift

Length of output: 2748


🏁 Script executed:

# Get the test file context around lines 57-58
sed -n '35,75p' control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go

Repository: openshift/hypershift

Length of output: 1305


Fix IP address handling in etcd peer certificate generation.

Lines 50–51 in etcd.go incorrectly add IP addresses ("127.0.0.1", "::1") to the dnsNames list. Per X.509 standards, IP addresses must be in the IPAddresses field, not DNSNames. The function signature supports a separate ips parameter for this purpose.

Fix the implementation by passing IPs via the dedicated parameter:

return reconcileSignedCertWithKeysAndAddresses(secret, ca, ownerRef, "etcd-discovery", []string{"kubernetes"}, X509UsageClientServerAuth, EtcdPeerCrtKey, EtcdPeerKeyKey, "", dnsNames, []string{"127.0.0.1", "::1"}, "")

Then update the test assertion to check the correct certificate field:

g.Expect(cert.IPAddresses).To(ContainElements(
    net.ParseIP("127.0.0.1"),
    net.ParseIP("::1"),
))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@control-plane-operator/controllers/hostedcontrolplane/pki/etcd_test.go`
around lines 57 - 58, The etcd peer cert is currently placing "127.0.0.1" and
"::1" into DNSNames; move those into the IPAddresses parameter when calling
reconcileSignedCertWithKeysAndAddresses by removing them from dnsNames and
passing them as the ips argument (use []string{"127.0.0.1", "::1"}) in the
reconcileSignedCertWithKeysAndAddresses call for "etcd-discovery"; then adjust
the test in etcd_test.go to assert on cert.IPAddresses (using net.ParseIP for
each IP) instead of cert.DNSNames so the test verifies IPs are present in the
certificate's IPAddresses field.

))
})

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,
))
})
}