Skip to content

Commit 6b9d66c

Browse files
committed
fix: preserve registry addon root CA when moving management cluster
1 parent dcaa2e0 commit 6b9d66c

File tree

6 files changed

+247
-21
lines changed

6 files changed

+247
-21
lines changed

charts/cluster-api-runtime-extensions-nutanix/templates/certificates.yaml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,3 @@ spec:
3232
kind: {{ .Values.certificates.issuer.kind }}
3333
name: {{ template "chart.issuerName" . }}
3434
secretName: {{ template "chart.name" . }}-admission-tls
35-
---
36-
# CA used to sign certificates for the clusters' registry addons
37-
apiVersion: cert-manager.io/v1
38-
kind: Certificate
39-
metadata:
40-
name: registry-addon-root-ca
41-
namespace: {{ .Release.Namespace }}
42-
labels:
43-
{{- include "chart.labels" . | nindent 4 }}
44-
spec:
45-
isCA: true
46-
commonName: registry-addon
47-
secretName: registry-addon-root-ca
48-
issuerRef:
49-
kind: {{ .Values.certificates.issuer.kind }}
50-
name: {{ template "chart.issuerName" . }}
51-
duration: 87600h # 10 years

common/pkg/capi/utils/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
// ManagementCluster returns a Cluster object if c is pointing to a management cluster, otherwise returns nil.
18-
func ManagementCluster(ctx context.Context, c client.Client) (*clusterv1.Cluster, error) {
18+
func ManagementCluster(ctx context.Context, c client.Reader) (*clusterv1.Cluster, error) {
1919
allNodes := &corev1.NodeList{}
2020
err := c.List(ctx, allNodes)
2121
if err != nil {

pkg/handlers/generic/lifecycle/registry/cncfdistribution/handler.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ func (n *CNCFDistribution) Setup(
7373
cluster *clusterv1.Cluster,
7474
log logr.Logger,
7575
) error {
76+
log.Info("Setting up root CA for CNCF Distribution registry if not already present")
77+
err := utils.EnsureRegistryAddonRootCASecret(ctx, n.client, cluster)
78+
if err != nil {
79+
return fmt.Errorf("failed to ensure root CA secret for CNCF Distribution registry addon: %w", err)
80+
}
81+
7682
log.Info("Setting up CA for CNCF Distribution registry")
77-
err := utils.EnsureCASecretForCluster(
83+
err = utils.EnsureCASecretForCluster(
7884
ctx,
7985
n.client,
8086
cluster,

pkg/handlers/generic/lifecycle/registry/utils/tls.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,105 @@ const (
3030
)
3131

3232
var (
33+
// Valid for 10 years.
34+
defaultRootCADuration = 10 * 365 * 24 * time.Hour
35+
3336
// Similar to CAPI, set the NotBefore to a few minutes in the past to account for clock skew.
3437
// This cert is being generated on the management cluster, but used by a workload cluster.
3538
defaultCertificateNotBeforeSkew = 5 * time.Minute
3639
// Valid for 2 years to avoid expiring before the cluster is upgraded.
3740
defaultCertificateDuration = 2 * 365 * 24 * time.Hour
3841
)
3942

43+
// EnsureRegistryAddonRootCASecret ensures that the registry addon root CA secret exists.
44+
// This Secret is used to sign the registry TLS certificates for the remote clusters.
45+
func EnsureRegistryAddonRootCASecret(
46+
ctx context.Context,
47+
c ctrlclient.Client,
48+
managementCluster *clusterv1.Cluster,
49+
) error {
50+
globalTLSCertificateSecret, err := handlersutils.SecretForRegistryAddonRootCA(ctx, c)
51+
if err != nil && !handlersutils.IsSecretNotFoundError(err) {
52+
return err
53+
}
54+
// If the secret already exists, we don't need to do anything.
55+
if globalTLSCertificateSecret != nil {
56+
return nil
57+
}
58+
59+
certPEM, keyPEM, err := generateRegistryAddonRootCAData()
60+
if err != nil {
61+
return fmt.Errorf("failed to generate registry addon root CA data: %w", err)
62+
}
63+
rootCASecret := buildRegistryAddonRootCASecret(certPEM, keyPEM, managementCluster)
64+
65+
err = handlersutils.EnsureSecretForLocalCluster(ctx, c, rootCASecret, managementCluster)
66+
if err != nil {
67+
return fmt.Errorf("failed to ensure registry addon root CA secret: %w", err)
68+
}
69+
70+
return nil
71+
}
72+
73+
func generateRegistryAddonRootCAData() (certPEM, keyPEM []byte, err error) {
74+
// 1. generate a new RSA private key.
75+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
76+
if err != nil {
77+
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
78+
}
79+
80+
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
81+
if err != nil {
82+
return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
83+
}
84+
85+
// 2. create a self-signed CA certificate.
86+
template := &x509.Certificate{
87+
SerialNumber: serialNumber,
88+
Subject: pkix.Name{
89+
CommonName: "registry-addon",
90+
},
91+
NotBefore: time.Now().Add(-1 * defaultCertificateNotBeforeSkew),
92+
NotAfter: time.Now().Add(defaultRootCADuration),
93+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
94+
IsCA: true,
95+
BasicConstraintsValid: true,
96+
}
97+
98+
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
99+
if err != nil {
100+
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
101+
}
102+
103+
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
104+
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
105+
106+
return certPEM, keyPEM, nil
107+
}
108+
109+
func buildRegistryAddonRootCASecret(
110+
certPEM,
111+
keyPEM []byte,
112+
managementCluster *clusterv1.Cluster,
113+
) *corev1.Secret {
114+
data := map[string][]byte{
115+
caCrtKey: certPEM,
116+
corev1.TLSCertKey: certPEM,
117+
corev1.TLSPrivateKeyKey: keyPEM,
118+
}
119+
return &corev1.Secret{
120+
TypeMeta: metav1.TypeMeta{
121+
APIVersion: corev1.SchemeGroupVersion.String(),
122+
Kind: "Secret",
123+
},
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: handlersutils.RegistryAddonRootCASecretName,
126+
Namespace: managementCluster.Namespace,
127+
},
128+
Data: data,
129+
}
130+
}
131+
40132
type EnsureCertificateOpts struct {
41133
// RemoteSecretKey is the name and namespace of the TLS secret to be created on the remote cluster.
42134
RemoteSecretKey ctrlclient.ObjectKey

pkg/handlers/generic/lifecycle/registry/utils/tls_integration_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"sigs.k8s.io/cluster-api/controllers/remote"
1919
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2020

21+
handlersutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/utils"
2122
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/test/helpers"
2223
)
2324

@@ -73,6 +74,98 @@ FYnq6/jDVxCbWmmP2u4TT557gMqao0DaJstf/NSXlK0bhA2B64M=
7374
testRegistryAddonRootCASecretName = "registry-addon-root-ca"
7475
)
7576

77+
var _ = Describe("Test EnsureRegistryAddonRootCASecret", func() {
78+
clientScheme := runtime.NewScheme()
79+
utilruntime.Must(clientgoscheme.AddToScheme(clientScheme))
80+
utilruntime.Must(clusterv1.AddToScheme(clientScheme))
81+
82+
It("new root CA Secret should be created", func(ctx SpecContext) {
83+
c, err := helpers.TestEnv.GetK8sClientWithScheme(clientScheme)
84+
Expect(err).To(BeNil())
85+
86+
cluster := &clusterv1.Cluster{
87+
ObjectMeta: metav1.ObjectMeta{
88+
GenerateName: "test-management-cluster-",
89+
Namespace: corev1.NamespaceDefault,
90+
},
91+
}
92+
Expect(c.Create(ctx, cluster)).To(Succeed())
93+
94+
err = EnsureRegistryAddonRootCASecret(ctx, c, cluster)
95+
Expect(err).To(Succeed())
96+
97+
// Verify the global TLS certificate secret is created.
98+
globalTLSCertificateSecret, err := handlersutils.SecretForRegistryAddonRootCA(ctx, c)
99+
Expect(err).To(Succeed())
100+
Expect(globalTLSCertificateSecret.OwnerReferences).To(
101+
ContainElement(
102+
metav1.OwnerReference{
103+
APIVersion: clusterv1.GroupVersion.String(),
104+
Kind: clusterv1.ClusterKind,
105+
Name: cluster.Name,
106+
UID: cluster.UID,
107+
},
108+
),
109+
)
110+
Expect(globalTLSCertificateSecret.Data["ca.crt"]).ToNot(BeEmpty())
111+
Expect(globalTLSCertificateSecret.Data[corev1.TLSCertKey]).ToNot(BeEmpty())
112+
Expect(globalTLSCertificateSecret.Data[corev1.TLSPrivateKeyKey]).ToNot(BeEmpty())
113+
})
114+
115+
It("a root CA Secret should not be re-created", func(ctx SpecContext) {
116+
c, err := helpers.TestEnv.GetK8sClientWithScheme(clientScheme)
117+
Expect(err).To(BeNil())
118+
119+
cluster := &clusterv1.Cluster{
120+
ObjectMeta: metav1.ObjectMeta{
121+
GenerateName: "test-management-cluster-",
122+
Namespace: corev1.NamespaceDefault,
123+
},
124+
}
125+
Expect(c.Create(ctx, cluster)).To(Succeed())
126+
127+
err = EnsureRegistryAddonRootCASecret(ctx, c, cluster)
128+
Expect(err).To(Succeed())
129+
130+
globalTLSCertificateSecret, err := handlersutils.SecretForRegistryAddonRootCA(ctx, c)
131+
Expect(err).To(Succeed())
132+
Expect(globalTLSCertificateSecret.Data["ca.crt"]).ToNot(BeEmpty())
133+
Expect(globalTLSCertificateSecret.Data[corev1.TLSCertKey]).ToNot(BeEmpty())
134+
Expect(globalTLSCertificateSecret.Data[corev1.TLSPrivateKeyKey]).ToNot(BeEmpty())
135+
data := globalTLSCertificateSecret.Data
136+
137+
// Verify the data is not changed when running the function again.
138+
err = EnsureRegistryAddonRootCASecret(ctx, c, cluster)
139+
Expect(err).To(Succeed())
140+
141+
globalTLSCertificateSecret, err = handlersutils.SecretForRegistryAddonRootCA(ctx, c)
142+
Expect(err).To(Succeed())
143+
144+
Expect(globalTLSCertificateSecret.Data).To(Equal(data))
145+
})
146+
AfterEach(func(ctx SpecContext) {
147+
c, err := helpers.TestEnv.GetK8sClientWithScheme(clientScheme)
148+
Expect(err).To(BeNil())
149+
150+
globalSecret := &corev1.Secret{
151+
TypeMeta: metav1.TypeMeta{
152+
APIVersion: corev1.SchemeGroupVersion.String(),
153+
Kind: "Secret",
154+
},
155+
ObjectMeta: metav1.ObjectMeta{
156+
Name: handlersutils.RegistryAddonRootCASecretName,
157+
Namespace: corev1.NamespaceDefault,
158+
},
159+
}
160+
Expect(c.Delete(ctx, globalSecret)).To(
161+
Or(
162+
Succeed(),
163+
MatchError("secrets \"registry-addon-root-ca\" not found"),
164+
),
165+
)
166+
})
167+
})
168+
76169
var _ = Describe("Test EnsureCASecretForCluster", func() {
77170
clientScheme := runtime.NewScheme()
78171
utilruntime.Must(clientgoscheme.AddToScheme(clientScheme))

pkg/handlers/utils/secrets.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package utils
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910

1011
corev1 "k8s.io/api/core/v1"
@@ -19,7 +20,7 @@ import (
1920
)
2021

2122
const (
22-
registryAddonRootCASecretName = "registry-addon-root-ca"
23+
RegistryAddonRootCASecretName = "registry-addon-root-ca"
2324
)
2425

2526
// CopySecretToRemoteCluster will get the Secret from srcSecretName
@@ -157,7 +158,7 @@ func SecretForRegistryAddonRootCA(
157158
ctx context.Context,
158159
c ctrlclient.Reader,
159160
) (*corev1.Secret, error) {
160-
secret, err := getSecret(ctx, c, registryAddonRootCASecretName, GetDeploymentNamespace())
161+
secret, err := findSecret(ctx, c, RegistryAddonRootCASecretName)
161162
if err != nil {
162163
return nil, fmt.Errorf("error getting registry addon root CA secret: %w", err)
163164
}
@@ -208,3 +209,54 @@ func getSecret(
208209
}
209210
return secret, c.Get(ctx, ctrlclient.ObjectKeyFromObject(secret), secret)
210211
}
212+
213+
type ErrSecretNotFound struct {
214+
name string
215+
}
216+
217+
func (e *ErrSecretNotFound) Error() string {
218+
return fmt.Sprintf("secrets %q not found", e.name)
219+
}
220+
221+
func IsSecretNotFoundError(err error) bool {
222+
var e *ErrSecretNotFound
223+
return errors.As(err, &e)
224+
}
225+
226+
type ErrMultipleSecretsFound struct {
227+
name string
228+
}
229+
230+
func (e *ErrMultipleSecretsFound) Error() string {
231+
return fmt.Sprintf("multiple Secrets found with name %q", e.name)
232+
}
233+
234+
// findSecret finds a Secret by name across all namespaces.
235+
// It returns an error if multiple Secrets with the same name are found.
236+
// Returns nil if no Secret is found.
237+
func findSecret(
238+
ctx context.Context,
239+
c ctrlclient.Reader,
240+
secretName string,
241+
) (*corev1.Secret, error) {
242+
secrets := &corev1.SecretList{}
243+
err := c.List(ctx, secrets)
244+
if err != nil {
245+
return nil, fmt.Errorf("error listing secrets: %w", err)
246+
}
247+
248+
matches := make([]corev1.Secret, 0)
249+
for _, secret := range secrets.Items {
250+
if secret.Name == secretName {
251+
matches = append(matches, secret)
252+
}
253+
}
254+
switch len(matches) {
255+
case 1:
256+
return &matches[0], nil
257+
case 0:
258+
return nil, &ErrSecretNotFound{name: secretName}
259+
default:
260+
return nil, &ErrMultipleSecretsFound{name: secretName}
261+
}
262+
}

0 commit comments

Comments
 (0)