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/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/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/online.go b/pkg/controllers/online.go index 4b1d94a..7ada58a 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, @@ -387,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) @@ -397,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 } diff --git a/pkg/controllers/resources.go b/pkg/controllers/resources.go index 73a37de..8e32765 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,17 +268,25 @@ 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 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(), @@ -283,6 +312,40 @@ 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 { + if !apierrors.IsNotFound(err) { + return nil, 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)