Skip to content
Merged
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,9 @@ 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),
// etcd-client uses a ClusterIP service (not headless), so it does not create per-pod
// PTR records and cannot cause reverse DNS ambiguity during peer TLS verification.
// TODO(OCPBUGS-86648): move IPs to the ips parameter of reconcileSignedCertWithKeysAndAddresses.
"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 etcd-discovery 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",
// TODO(OCPBUGS-86648): assert on cert.IPAddresses instead once IPs are moved out of dnsNames.
"127.0.0.1",
"::1",
Comment on lines +56 to +57

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.

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.

This is encoding the behavior that should be fixed. Mind after creating the JIRA card reference it here in a comment with a TODO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @sdminonne ,
I have added the TODO comment and raised the JIRA card(OCPBUGS-86648) as per the suggestion.

))

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.

NIT: g.Expect(cert.DNSNames).ToNot(ContainElement(ContainSubstring("etcd-client")))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @sdminonne , I have updated both the suggestions.

g.Expect(cert.DNSNames).ToNot(ContainElement(ContainSubstring("etcd-client")))
})

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ metadata:
app: etcd
name: etcd-client
spec:
clusterIP: None
ports:
- name: etcd-client
port: 2379
Expand Down