From a729a3ef164357c81c7aaa55987b9141d4473372 Mon Sep 17 00:00:00 2001 From: Dan Pock Date: Tue, 2 Jun 2026 15:49:40 -0400 Subject: [PATCH 1/4] Add support for handling registration URL certificates --- internal/consts/consts.go | 1 + internal/consts/names.go | 19 +++++--- internal/consts/secrets.go | 10 +++-- pkg/controllers/controller.go | 17 ++++++-- pkg/controllers/lifecycle/deciders.go | 4 ++ pkg/controllers/lifecycle/mutators.go | 8 ++++ pkg/controllers/resources.go | 63 +++++++++++++++++++++++++-- 7 files changed, 104 insertions(+), 18 deletions(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index cda3422..3c4a3a8 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -13,6 +13,7 @@ const ( FinalizerSccCredentials = "scc.cattle.io/managed-credentials" FinalizerSccRegistration = "scc.cattle.io/managed-registration" FinalizerSccRegistrationCode = "scc.cattle.io/managed-registration-code" + FinalizerSccRegistrationURLCert = "scc.cattle.io/managed-registration-url-cert" ) const ( diff --git a/internal/consts/names.go b/internal/consts/names.go index 264ef0a..d794ca2 100644 --- a/internal/consts/names.go +++ b/internal/consts/names.go @@ -4,13 +4,14 @@ import "fmt" // Secret names and name prefixes const ( - ResourceSCCEntrypointSecretName = "scc-registration" - SCCMetricsOutputSecretName = "rancher-scc-metrics" - RancherMetricsSecretRequestName = SCCMetricsOutputSecretName - SCCSystemCredentialsSecretNamePrefix = "scc-system-credentials-" - RegistrationCodeSecretNamePrefix = "registration-code-" - OfflineRequestSecretNamePrefix = "offline-request-" - OfflineCertificateSecretNamePrefix = "offline-certificate-" + ResourceSCCEntrypointSecretName = "scc-registration" + SCCMetricsOutputSecretName = "rancher-scc-metrics" + RancherMetricsSecretRequestName = SCCMetricsOutputSecretName + SCCSystemCredentialsSecretNamePrefix = "scc-system-credentials-" + RegistrationCodeSecretNamePrefix = "registration-code-" + OfflineRequestSecretNamePrefix = "offline-request-" + OfflineCertificateSecretNamePrefix = "offline-certificate-" + RegistrationURLCertificateSecretNamePrefix = "registration-url-cert-" ) func RegistrationName(namePartIn string) string { @@ -32,3 +33,7 @@ func OfflineRequestSecretName(namePartIn string) string { func OfflineCertificateSecretName(namePartIn string) string { return fmt.Sprintf("%s%s", OfflineCertificateSecretNamePrefix, namePartIn) } + +func RegistrationURLCertificateSecretName(namePartIn string) string { + return fmt.Sprintf("%s%s", RegistrationURLCertificateSecretNamePrefix, namePartIn) +} diff --git a/internal/consts/secrets.go b/internal/consts/secrets.go index e847f30..1b25bdb 100644 --- a/internal/consts/secrets.go +++ b/internal/consts/secrets.go @@ -6,13 +6,15 @@ const ( SecretKeyOfflineRegRequest = "request" SecretKeyOfflineRegCert = "certificate" RegistrationURL = "registrationUrl" + RegistrationURLCert = "registrationUrlCert" ) type SecretRole string const ( - SCCCredentialsRole SecretRole = "scc-credentials" - RegistrationCode SecretRole = "reg-code" - OfflineRequestRole SecretRole = "offline-request" - OfflineCertificate SecretRole = "offline-certificate" + SCCCredentialsRole SecretRole = "scc-credentials" + RegistrationCode SecretRole = "reg-code" + OfflineRequestRole SecretRole = "offline-request" + OfflineCertificate SecretRole = "offline-certificate" + RegistrationServerCertRole SecretRole = "registration-server-cert" ) diff --git a/pkg/controllers/controller.go b/pkg/controllers/controller.go index 269b1f5..ad5733e 100644 --- a/pkg/controllers/controller.go +++ b/pkg/controllers/controller.go @@ -207,7 +207,7 @@ func (h *handler) OnSecretChange(_ string, incomingObj *corev1.Secret) (*corev1. // This only applies to the SCC Entrypoint secrets - currently only used for/by Rancher // This will adopt "unowned" secrets and ignore any that are owned by other operators if !helpers.ShouldManage(incomingObj, h.options.OperatorName) { - // When the secret has no managedBy label, we should assume ownership I guess? + // When the secret has no managedBy label, we should assume ownership if !helpers.HasManagedByLabel(incomingObj) { h.log.Debugf("taking ownership of the unowned entrypoint secret") prepared := incomingObj.DeepCopy() @@ -258,8 +258,7 @@ func (h *handler) OnSecretChange(_ string, incomingObj *corev1.Secret) (*corev1. return incomingObj, nil } - // If secret hash has changed make sure that we submit objects that correspond to that hash - // are cleaned up + // Upon secret hash changes ensure that objects which correspond to that hash are cleaned up // TODO: make it so that changes to the incoming Salt (which changes the nameID) are correctly handled // Note that change would affect both name and content hashes - however something seems to not. if incomingNameHash != params.nameID { @@ -319,6 +318,18 @@ func (h *handler) OnSecretChange(_ string, incomingObj *corev1.Secret) (*corev1. if _, err := h.secretRepo.CreateOrUpdateSecret(regCodeSecret); err != nil { return incomingObj, err } + + // TODO - maybe pull this out if RMT expects to support offline certs too? + if params.hasRegURLCertData { + regCodeSecret, err := h.regURLCertFromSecretEntrypoint(params) + if err != nil { + return incomingObj, err + } + + if _, err := h.secretRepo.CreateOrUpdateSecret(regCodeSecret); err != nil { + return incomingObj, err + } + } } // construct associated registration CRs diff --git a/pkg/controllers/lifecycle/deciders.go b/pkg/controllers/lifecycle/deciders.go index df7bd5c..98fe313 100644 --- a/pkg/controllers/lifecycle/deciders.go +++ b/pkg/controllers/lifecycle/deciders.go @@ -67,3 +67,7 @@ func SecretHasCredentialsFinalizer(objIn *corev1.Secret) bool { func SecretHasRegCodeFinalizer(objIn *corev1.Secret) bool { return hasFinalizer(objIn, consts.FinalizerSccRegistrationCode) } + +func SecretHasRegURLCertFinalizer(objIn *corev1.Secret) bool { + return hasFinalizer(objIn, consts.FinalizerSccRegistrationURLCert) +} diff --git a/pkg/controllers/lifecycle/mutators.go b/pkg/controllers/lifecycle/mutators.go index ce91710..23233de 100644 --- a/pkg/controllers/lifecycle/mutators.go +++ b/pkg/controllers/lifecycle/mutators.go @@ -82,3 +82,11 @@ func SecretAddOfflineFinalizer(secret *corev1.Secret) *corev1.Secret { func SecretRemoveOfflineFinalizer(secret *corev1.Secret) *corev1.Secret { return runtimeRemoveFinalizer[*corev1.Secret](secret, consts.FinalizerSccOfflineSecret) } + +func SecretAddRegURLCertFinalizer(secret *corev1.Secret) *corev1.Secret { + return runtimeAddFinalizer[*corev1.Secret](secret, consts.FinalizerSccRegistrationURLCert) +} + +func SecretRemoveRegURLCertFinalizer(secret *corev1.Secret) *corev1.Secret { + return runtimeRemoveFinalizer[*corev1.Secret](secret, consts.FinalizerSccRegistrationURLCert) +} diff --git a/pkg/controllers/resources.go b/pkg/controllers/resources.go index 73a37de..506e625 100644 --- a/pkg/controllers/resources.go +++ b/pkg/controllers/resources.go @@ -89,6 +89,15 @@ func getCurrentRegURL(secret *corev1.Secret) (regURL []byte) { return []byte{} } +func getRegURLCert(secret *corev1.Secret) ([]byte, bool) { + regCertBytes, ok := secret.Data[consts.RegistrationURLCert] + if ok { + return regCertBytes, ok + } + + return []byte{}, ok +} + // extractRegistrationParamsFromSecret will extract secret data and prepare it into a RegistrationParams func extractRegistrationParamsFromSecret(secret *corev1.Secret, managedByName string) (RegistrationParams, error) { extractParamsLog := logging.NewComponentLogger("params-extractor") @@ -117,12 +126,13 @@ func extractRegistrationParamsFromSecret(secret *corev1.Secret, managedByName st offlineRegCertData, certOk := secret.Data[consts.SecretKeyOfflineRegCert] hasOfflineCert := certOk && len(offlineRegCertData) > 0 - // TODO: when RMT needs to be supported eventually we need to accept Reg URL and Reg Server Cert. - var regURLBytes []byte + hasRegCertField := false + var regURLBytes, regCertBytes []byte regURLString := "" if regMode == v1.RegistrationModeOnline { regURLBytes = getCurrentRegURL(secret) regURLString = string(regURLBytes) + regCertBytes, hasRegCertField = getRegURLCert(secret) } hasher := md5.New() @@ -160,7 +170,14 @@ func extractRegistrationParamsFromSecret(secret *corev1.Secret, managedByName st Name: consts.OfflineCertificateSecretName(nameID), Namespace: secret.Namespace, }, - regURL: regURLString, + regURL: regURLString, + regURLCertSet: hasRegCertField, + hasRegURLCertData: hasRegCertField && len(regCertBytes) > 0, + regURLCertData: ®CertBytes, + regURLCertSecretRef: &corev1.SecretReference{ + Name: consts.RegistrationURLCertificateSecretName(nameID), + Namespace: secret.Namespace, + }, }, nil } @@ -172,6 +189,10 @@ type RegistrationParams struct { regCode []byte regCodeSecretRef *corev1.SecretReference regURL string + regURLCertSet bool // true when the secret includes regURLCert field + hasRegURLCertData bool // true when regURLCertSet and regURLCertData is not empty + regURLCertData *[]byte + regURLCertSecretRef *corev1.SecretReference hasOfflineCertData bool offlineCertData *[]byte offlineCertSecretRef *corev1.SecretReference @@ -247,12 +268,16 @@ func paramsToRegSpec(params RegistrationParams) v1.RegistrationSpec { // check if params has regURL and use, otherwise check if devmode and when true use staging Scc url if params.regURL != "" { regSpec.RegistrationRequest.RegistrationAPIUrl = ¶ms.regURL + + if params.hasRegURLCertData { + regSpec.RegistrationRequest.RegistrationAPICertificateSecretRef = params.regURLCertSecretRef + } } return regSpec } -// regCodeFromSecretEntrypoint fetches the registration code provided by an entrypoint secret +// regCodeFromSecretEntrypoint fetches (or prepares) the RegCode secret provided by an entrypoint secret func (h *handler) regCodeFromSecretEntrypoint(params RegistrationParams) (*corev1.Secret, error) { secretName := params.regCodeSecretRef.Name @@ -283,6 +308,36 @@ func (h *handler) regCodeFromSecretEntrypoint(params RegistrationParams) (*corev return regcodeSecret, nil } +func (h *handler) regURLCertFromSecretEntrypoint(params RegistrationParams) (*corev1.Secret, error) { + secretName := params.regURLCertSecretRef.Name + + regURLCertSecret, err := h.secretRepo.Cache.Get(h.options.SystemNamespace(), secretName) + if err != nil && apierrors.IsNotFound(err) { + regURLCertSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: h.options.SystemNamespace(), + Name: secretName, + }, + Data: map[string][]byte{ + consts.RegistrationURLCert: *params.regURLCertData, + }, + } + } + + if regURLCertSecret.Labels == nil { + regURLCertSecret.Labels = map[string]string{} + } + defaultLabels := params.Labels() + defaultLabels[consts.LabelSccSecretRole] = string(consts.RegistrationServerCertRole) + maps.Copy(regURLCertSecret.Labels, defaultLabels) + + if !lifecycle.SecretHasRegURLCertFinalizer(regURLCertSecret) { + regURLCertSecret = lifecycle.SecretAddRegURLCertFinalizer(regURLCertSecret) + } + + return regURLCertSecret, nil +} + // offlineCertFromSecretEntrypoint helps to extract and prepare the Offline Cert secret for creation based on entrypoint secret func (h *handler) offlineCertFromSecretEntrypoint(params RegistrationParams) (*corev1.Secret, error) { secretName := consts.OfflineCertificateSecretName(params.nameID) From 5d7a2c531eba58294f6bfa23c0c14ebeabc3f4f1 Mon Sep 17 00:00:00 2001 From: Dan Pock Date: Tue, 2 Jun 2026 15:56:17 -0400 Subject: [PATCH 2/4] Handle non-NotFound errors when fetching secrets in resource controller --- pkg/controllers/resources.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/controllers/resources.go b/pkg/controllers/resources.go index 506e625..8e32765 100644 --- a/pkg/controllers/resources.go +++ b/pkg/controllers/resources.go @@ -282,7 +282,11 @@ func (h *handler) regCodeFromSecretEntrypoint(params RegistrationParams) (*corev secretName := params.regCodeSecretRef.Name regcodeSecret, err := h.secretRepo.Cache.Get(h.options.SystemNamespace(), secretName) - if err != nil && apierrors.IsNotFound(err) { + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + regcodeSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: h.options.SystemNamespace(), @@ -312,7 +316,11 @@ func (h *handler) regURLCertFromSecretEntrypoint(params RegistrationParams) (*co secretName := params.regURLCertSecretRef.Name regURLCertSecret, err := h.secretRepo.Cache.Get(h.options.SystemNamespace(), secretName) - if err != nil && apierrors.IsNotFound(err) { + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + regURLCertSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: h.options.SystemNamespace(), From 53a1f666d216d02bfe6a87b8e2d6be7eef56fc54 Mon Sep 17 00:00:00 2001 From: Dan Pock Date: Tue, 2 Jun 2026 16:13:52 -0400 Subject: [PATCH 3/4] Add support for using registration URL certificates in SCC connections --- internal/suseconnect/util.go | 39 ++++++++++++++++++++++++++++ internal/suseconnect/wrapper.go | 12 +++++---- internal/suseconnect/wrapper_test.go | 4 +-- pkg/controllers/online.go | 9 ++++++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/internal/suseconnect/util.go b/internal/suseconnect/util.go index 6151dcc..003bf77 100644 --- a/internal/suseconnect/util.go +++ b/internal/suseconnect/util.go @@ -1,6 +1,9 @@ package suseconnect import ( + "crypto/x509" + "encoding/pem" + corev1 "k8s.io/api/core/v1" "github.com/rancher/scc-operator/internal/consts" @@ -24,3 +27,39 @@ func FetchSccRegistrationCodeFrom(secretRepo *secretrepo.SecretRepository, refer return string(regCode) } + +// FetchRegistrationURLCertFrom fetches and parses the registration URL certificate from a secret +func FetchRegistrationURLCertFrom(secretRepo *secretrepo.SecretRepository, reference *corev1.SecretReference) *x509.Certificate { + if reference == nil { + return nil + } + + sccContextLogger().Debugf("Fetching Registration URL Certificate from secret %s/%s", reference.Namespace, reference.Name) + certSecret, err := secretRepo.Cache.Get(reference.Namespace, reference.Name) + if err != nil { + sccContextLogger().Warnf("Failed to get Registration URL Certificate from secret %s/%s: %v", reference.Namespace, reference.Name, err) + return nil + } + sccContextLogger().Debugf("Found certificate secret %s/%s", reference.Namespace, reference.Name) + + certData, ok := certSecret.Data[consts.RegistrationURLCert] + if !ok { + sccContextLogger().Warnf("registration URL cert secret `%v` does not contain expected data `%s`", reference, consts.RegistrationURLCert) + return nil + } + + // Parse PEM encoded certificate + block, _ := pem.Decode(certData) + if block == nil { + sccContextLogger().Warnf("failed to decode PEM certificate from secret %s/%s", reference.Namespace, reference.Name) + return nil + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + sccContextLogger().Warnf("failed to parse x509 certificate from secret %s/%s: %v", reference.Namespace, reference.Name, err) + return nil + } + + return cert +} diff --git a/internal/suseconnect/wrapper.go b/internal/suseconnect/wrapper.go index 2807164..4344786 100644 --- a/internal/suseconnect/wrapper.go +++ b/internal/suseconnect/wrapper.go @@ -1,6 +1,7 @@ package suseconnect import ( + "crypto/x509" "fmt" "github.com/SUSE/connect-ng/pkg/connection" @@ -25,11 +26,12 @@ type SccWrapper struct { rancherMetrics telemetry.MetricsWrapper } -func DefaultConnectionOptions(appName, version string) connection.Options { - // So this doesn't necessarily mean these have to match Rancher on the cluster. - // Rather the details about the HTTP client talking to SCC - // TODO: eventually add localization support? - return connection.DefaultOptions(appName, version, "en_US") +func DefaultConnectionOptions(appName, version string, cert *x509.Certificate) connection.Options { + opts := connection.DefaultOptions(appName, version, "en_US") + if cert != nil { + opts.Certificate = cert + } + return opts } type OnlineConnectionParams struct { diff --git a/internal/suseconnect/wrapper_test.go b/internal/suseconnect/wrapper_test.go index 5052f7f..ee5bf68 100644 --- a/internal/suseconnect/wrapper_test.go +++ b/internal/suseconnect/wrapper_test.go @@ -8,7 +8,7 @@ import ( ) func TestDefaultConnectionOptionsBasic(t *testing.T) { - defaultOptions := DefaultConnectionOptions("rancher-scc-integration", "0.0.1") + defaultOptions := DefaultConnectionOptions("rancher-scc-integration", "0.0.1", nil) expected := connection.Options{ URL: connection.DefaultBaseURL, Secure: true, @@ -21,7 +21,7 @@ func TestDefaultConnectionOptionsBasic(t *testing.T) { } func TestDefaultConnectionOptions(t *testing.T) { - defaultOptions := DefaultConnectionOptions("rancher-scc-integration", "0.0.1") + defaultOptions := DefaultConnectionOptions("rancher-scc-integration", "0.0.1", nil) assert.Equal(t, connection.DefaultBaseURL, defaultOptions.URL) assert.Equal(t, "rancher-scc-integration", defaultOptions.AppName) assert.Equal(t, "0.0.1", defaultOptions.Version) diff --git a/pkg/controllers/online.go b/pkg/controllers/online.go index 4b1d94a..9ef9742 100644 --- a/pkg/controllers/online.go +++ b/pkg/controllers/online.go @@ -1,6 +1,7 @@ package controllers import ( + "crypto/x509" "errors" "fmt" "net/http" @@ -43,11 +44,17 @@ func (s *sccOnlineMode) prepareSCCOnlineConnection( rancherMetrics telemetry.MetricsWrapper, registrationURL string, ) suseconnect.SccWrapper { + // Fetch the registration URL certificate if provided + var cert *x509.Certificate + if s.registration.Spec.RegistrationRequest.RegistrationAPICertificateSecretRef != nil { + cert = suseconnect.FetchRegistrationURLCertFrom(s.secretRepo, s.registration.Spec.RegistrationRequest.RegistrationAPICertificateSecretRef) + } + return suseconnect.OnlineRancherConnection( suseconnect.OnlineConnectionParams{ RancherURL: s.rancherURL, RegistrationURL: registrationURL, - Options: suseconnect.DefaultConnectionOptions(s.options.OperatorName, s.options.OperatorMetadata.Version), + Options: suseconnect.DefaultConnectionOptions(s.options.OperatorName, s.options.OperatorMetadata.Version, cert), }, s.sccCredentials.SccCredentials(), rancherMetrics, From 39118957b40f0b5f5b88da3d4d63c2d56f82b248 Mon Sep 17 00:00:00 2001 From: Dan Pock Date: Tue, 2 Jun 2026 16:19:09 -0400 Subject: [PATCH 4/4] Handle cleanup of registration URL certificate secrets and improve error handling in resource controller --- pkg/controllers/online.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pkg/controllers/online.go b/pkg/controllers/online.go index 9ef9742..7ada58a 100644 --- a/pkg/controllers/online.go +++ b/pkg/controllers/online.go @@ -394,7 +394,7 @@ func (s *sccOnlineMode) Deregister() error { if regCodeErr != nil && !apierrors.IsNotFound(regCodeErr) { return regCodeErr } - if lifecycle.SecretHasRegCodeFinalizer(regCodeSecret) { + if regCodeSecret != nil && lifecycle.SecretHasRegCodeFinalizer(regCodeSecret) { updateRegCodeSecret := regCodeSecret.DeepCopy() updateRegCodeSecret = lifecycle.SecretRemoveRegCodeFinalizer(updateRegCodeSecret) @@ -404,10 +404,32 @@ func (s *sccOnlineMode) Deregister() error { } } - if err := s.secretRepo.Controller.Delete(regCodeSecretRef.Namespace, regCodeSecretRef.Name, &metav1.DeleteOptions{}); err != nil { + if err := s.secretRepo.Controller.Delete(regCodeSecretRef.Namespace, regCodeSecretRef.Name, &metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { return err } + // Clean up registration URL certificate secret if it exists + regURLCertSecretRef := s.registration.Spec.RegistrationRequest.RegistrationAPICertificateSecretRef + if regURLCertSecretRef != nil { + regURLCertSecret, regURLCertErr := s.secretRepo.Get(regURLCertSecretRef.Namespace, regURLCertSecretRef.Name) + if regURLCertErr != nil && !apierrors.IsNotFound(regURLCertErr) { + return regURLCertErr + } + if regURLCertSecret != nil && lifecycle.SecretHasRegURLCertFinalizer(regURLCertSecret) { + updateRegURLCertSecret := regURLCertSecret.DeepCopy() + updateRegURLCertSecret = lifecycle.SecretRemoveRegURLCertFinalizer(updateRegURLCertSecret) + + _, regURLCertErr = s.secretRepo.Controller.Update(updateRegURLCertSecret) + if regURLCertErr != nil { + return regURLCertErr + } + } + + if err := s.secretRepo.Controller.Delete(regURLCertSecretRef.Namespace, regURLCertSecretRef.Name, &metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + return err + } + } + return nil }