diff --git a/apis/authentication/v1beta1/tenant_types.go b/apis/authentication/v1beta1/tenant_types.go index a7fee5bbfc..fe69665d13 100644 --- a/apis/authentication/v1beta1/tenant_types.go +++ b/apis/authentication/v1beta1/tenant_types.go @@ -21,6 +21,19 @@ import ( liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" ) +// AuthzPolicy is the policy used by the cluster to authorize or reject an incoming ResourceSlice. +type AuthzPolicy string + +const ( + // KeysExchange indicates that a keys exchange must be performed before accepting any ResourceSlice. + KeysExchange AuthzPolicy = "KeysExchange" + // TolerateNoHandshake indicates that the local cluster accepts ResourceSlices even when there + // never have been a key exchange with the peer cluster. + TolerateNoHandshake AuthzPolicy = "TolerateNoHandshake" + // DefaultAuthzPolicy is the default authorization policy if nothing is provided. + DefaultAuthzPolicy AuthzPolicy = KeysExchange +) + // TenantResource is the name of the tenant resources. var TenantResource = "tenants" @@ -33,10 +46,23 @@ var TenantGroupResource = schema.GroupResource{Group: GroupVersion.Group, Resour // TenantGroupVersionResource is groupResourceVersion used to register these objects. var TenantGroupVersionResource = GroupVersion.WithResource(TenantResource) +// GetAuthzPolicyValue returns the value of the pointer to an AuthzPolicy type, if the pointer is nil it returns the default value. +func GetAuthzPolicyValue(policy *AuthzPolicy) AuthzPolicy { + if policy == nil { + return DefaultAuthzPolicy + } + return *policy +} + // TenantSpec defines the desired state of Tenant. type TenantSpec struct { // ClusterID is the id of the consumer cluster. ClusterID liqov1beta1.ClusterID `json:"clusterID,omitempty"` + // AuthzPolicy is the policy used by the cluster to authorize or reject an incoming ResourceSlice. + // Default is KeysExchange. + // +kubebuilder:validation:Enum=KeysExchange;TolerateNoHandshake + // +kubebuilder:default=KeysExchange + *AuthzPolicy `json:"authzPolicy,omitempty"` // PublicKey is the public key of the tenant cluster. PublicKey []byte `json:"publicKey,omitempty"` // CSR is the Certificate Signing Request of the tenant cluster. diff --git a/apis/authentication/v1beta1/zz_generated.deepcopy.go b/apis/authentication/v1beta1/zz_generated.deepcopy.go index d51ab0aa9b..7a1200350e 100644 --- a/apis/authentication/v1beta1/zz_generated.deepcopy.go +++ b/apis/authentication/v1beta1/zz_generated.deepcopy.go @@ -411,6 +411,11 @@ func (in *TenantList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { *out = *in + if in.AuthzPolicy != nil { + in, out := &in.AuthzPolicy, &out.AuthzPolicy + *out = new(AuthzPolicy) + **out = **in + } if in.PublicKey != nil { in, out := &in.PublicKey, &out.PublicKey *out = make([]byte, len(*in)) diff --git a/deployments/liqo/charts/liqo-crds/crds/authentication.liqo.io_tenants.yaml b/deployments/liqo/charts/liqo-crds/crds/authentication.liqo.io_tenants.yaml index f4375d42e0..561aaeff72 100644 --- a/deployments/liqo/charts/liqo-crds/crds/authentication.liqo.io_tenants.yaml +++ b/deployments/liqo/charts/liqo-crds/crds/authentication.liqo.io_tenants.yaml @@ -50,6 +50,15 @@ spec: spec: description: TenantSpec defines the desired state of Tenant. properties: + authzPolicy: + default: KeysExchange + description: |- + AuthzPolicy is the policy used by the cluster to authorize or reject an incoming ResourceSlice. + Default is KeysExchange. + enum: + - KeysExchange + - TolerateNoHandshake + type: string clusterID: description: ClusterID is the id of the consumer cluster. pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ diff --git a/internal/crdReplicator/crdReplicator-operator.go b/internal/crdReplicator/crdReplicator-operator.go index 486b2d295e..2a6ec689f5 100644 --- a/internal/crdReplicator/crdReplicator-operator.go +++ b/internal/crdReplicator/crdReplicator-operator.go @@ -16,6 +16,7 @@ package crdreplicator import ( "context" + "crypto/sha256" "fmt" corev1 "k8s.io/api/core/v1" @@ -39,6 +40,7 @@ import ( "github.com/liqotech/liqo/internal/crdReplicator/resources" "github.com/liqotech/liqo/pkg/consts" identitymanager "github.com/liqotech/liqo/pkg/identityManager" + "github.com/liqotech/liqo/pkg/utils/getters" traceutils "github.com/liqotech/liqo/pkg/utils/trace" ) @@ -107,13 +109,9 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (result ct // the object is being deleted if controllerutil.ContainsFinalizer(&secret, finalizer) { // close remote watcher for remote cluster - reflector, ok := c.Reflectors[remoteClusterID] - if ok { - if err := reflector.Stop(); err != nil { - klog.Errorf("%sFailed to stop reflection: %v", prefix, err) - return ctrl.Result{}, err - } - delete(c.Reflectors, remoteClusterID) + if err := c.stopReflector(remoteClusterID, false); err != nil { + klog.Errorf("%sFailed to stop reflection: %v", prefix, err) + return ctrl.Result{}, err } // remove the finalizer from the list and update it. @@ -139,17 +137,41 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (result ct } // Check if reflection towards the remote cluster has already been started. - if _, found := c.Reflectors[remoteClusterID]; found { + if reflector, found := c.Reflectors[remoteClusterID]; found { + // We ignore the case in which the secret lacks of the kubeconfig, as in that case we still want to delete the reflector + // and manage the error. + secretContent := secret.Data[consts.KubeconfigSecretField] + secretHash := c.hashSecretConfig(secretContent) + + // If there are no changes on the secret or on the remote namespace where the reflector operats, skip reconciliation. + if reflector.GetSecretHash() == secretHash && reflector.GetRemoteTenantNamespace() == remoteTenantNamespace { + return ctrl.Result{}, nil + } + + // If there have been a change on the secret, delete the secret to allow the creation of a new reflector. + klog.Infof("%sChanges detected on the control plane secret %q for clusterID %q: recreating reflector", + prefix, req.NamespacedName, remoteClusterID) + // Stop the reflection to update the reflector + if err := c.stopReflector(remoteClusterID, true); err != nil { + klog.Errorf("%sFailed to stop reflection: %v", prefix, err) + return ctrl.Result{}, err + } + } + + // We need to get the secret to make sure that there are not multiple secrets pointing to the same cluster + currSecret, err := getters.GetControlPlaneKubeconfigSecretByClusterID(ctx, c.Client, remoteClusterID) + if err != nil { + klog.Errorf("%sUnable to process secret for clusterID %q: %v", prefix, remoteClusterID, err) return ctrl.Result{}, nil } - config, err := c.IdentityReader.GetConfig(remoteClusterID, localTenantNamespace) + config, err := c.IdentityReader.GetConfigFromSecret(remoteClusterID, currSecret) if err != nil { klog.Errorf("%sUnable to retrieve config for clusterID %q: %v", prefix, remoteClusterID, err) return ctrl.Result{}, nil } - return ctrl.Result{}, c.setupReflectionToPeeringCluster(ctx, config, remoteClusterID, localTenantNamespace, remoteTenantNamespace) + return ctrl.Result{}, c.setupReflectionToPeeringCluster(ctx, currSecret, config, remoteClusterID, localTenantNamespace, remoteTenantNamespace) } // SetupWithManager registers a new controller for identity Secrets. @@ -220,7 +242,7 @@ func (c *Controller) ensureFinalizer(ctx context.Context, secret *corev1.Secret, return c.Client.Update(ctx, secret) } -func (c *Controller) setupReflectionToPeeringCluster(ctx context.Context, config *rest.Config, +func (c *Controller) setupReflectionToPeeringCluster(ctx context.Context, secret *corev1.Secret, config *rest.Config, remoteClusterID liqov1beta1.ClusterID, localNamespace, remoteNamespace string) error { dynamicClient, err := dynamic.NewForConfig(config) if err != nil { @@ -228,12 +250,36 @@ func (c *Controller) setupReflectionToPeeringCluster(ctx context.Context, config return err } - reflector := c.ReflectionManager.NewForRemote(dynamicClient, remoteClusterID, localNamespace, remoteNamespace) + secretHash := c.hashSecretConfig(secret.Data[consts.KubeconfigSecretField]) + + reflector := c.ReflectionManager.NewForRemote(dynamicClient, remoteClusterID, localNamespace, remoteNamespace, secretHash) reflector.Start(ctx) c.Reflectors[remoteClusterID] = reflector return nil } +func (c *Controller) stopReflector(remoteClusterID liqov1beta1.ClusterID, skipChecks bool) error { + reflector, ok := c.Reflectors[remoteClusterID] + if ok { + stopFn := reflector.Stop + // Use the StopForce function if we want to skip the checks + if skipChecks { + stopFn = reflector.StopForce + } + + if err := stopFn(); err != nil { + return err + } + delete(c.Reflectors, remoteClusterID) + } + return nil +} + +func (c *Controller) hashSecretConfig(secretData []byte) string { + hash := sha256.Sum256(secretData) + return fmt.Sprintf("%x", hash) +} + func (c *Controller) enforceReflectionStatus(ctx context.Context, remoteClusterID liqov1beta1.ClusterID, deleting bool) error { reflector, found := c.Reflectors[remoteClusterID] if !found { diff --git a/internal/crdReplicator/reflection/manager.go b/internal/crdReplicator/reflection/manager.go index 69967b17b1..2fa5a8d677 100644 --- a/internal/crdReplicator/reflection/manager.go +++ b/internal/crdReplicator/reflection/manager.go @@ -86,7 +86,8 @@ func (m *Manager) Start(ctx context.Context, registeredResources []resources.Res } // NewForRemote returns a new reflector for a given remote cluster. -func (m *Manager) NewForRemote(client dynamic.Interface, clusterID liqov1beta1.ClusterID, localNamespace, remoteNamespace string) *Reflector { +func (m *Manager) NewForRemote(client dynamic.Interface, clusterID liqov1beta1.ClusterID, localNamespace, remoteNamespace string, + secretHash string) *Reflector { return &Reflector{ manager: m, @@ -97,6 +98,8 @@ func (m *Manager) NewForRemote(client dynamic.Interface, clusterID liqov1beta1.C remoteNamespace: remoteNamespace, remoteClusterID: clusterID, + secretHash: secretHash, + resources: make(map[schema.GroupVersionResource]*reflectedResource), workqueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), } diff --git a/internal/crdReplicator/reflection/manager_test.go b/internal/crdReplicator/reflection/manager_test.go index e79d9d2ca3..f713ffb9c6 100644 --- a/internal/crdReplicator/reflection/manager_test.go +++ b/internal/crdReplicator/reflection/manager_test.go @@ -80,7 +80,7 @@ var _ = Describe("Manager tests", func() { Describe("the NewForRemote function", func() { var reflector *Reflector - JustBeforeEach(func() { reflector = manager.NewForRemote(remote, remoteClusterID, localNamespace, remoteNamespace) }) + JustBeforeEach(func() { reflector = manager.NewForRemote(remote, remoteClusterID, localNamespace, remoteNamespace, "") }) It("Should return a non nil reflector", func() { Expect(reflector).ToNot(BeNil()) }) It("Should correctly reference the parent manager", func() { Expect(reflector.manager).To(Equal(manager)) }) It("Should correctly populate the reflector fields", func() { diff --git a/internal/crdReplicator/reflection/reflector.go b/internal/crdReplicator/reflection/reflector.go index d5384d2b0a..26dcd8002f 100644 --- a/internal/crdReplicator/reflection/reflector.go +++ b/internal/crdReplicator/reflection/reflector.go @@ -56,6 +56,8 @@ type Reflector struct { resources map[schema.GroupVersionResource]*reflectedResource + secretHash string + workqueue workqueue.RateLimitingInterface cancel context.CancelFunc } @@ -72,6 +74,16 @@ type reflectedResource struct { initialized bool } +// GetRemoteTenantNamespace returns the remote namespace where the reflector reflects the resources. +func (r *Reflector) GetRemoteTenantNamespace() string { + return r.remoteNamespace +} + +// GetSecretHash returns the hash of the secret that generated this reflector. +func (r *Reflector) GetSecretHash() string { + return r.secretHash +} + // Start starts the reflection towards the remote cluster. func (r *Reflector) Start(ctx context.Context) { ctx, r.cancel = context.WithCancel(ctx) @@ -87,21 +99,15 @@ func (r *Reflector) Start(ctx context.Context) { }() } -// Stop stops the reflection towards the remote cluster, and removes the replicated resources. +// Stop stops the reflection towards the remote cluster, it returns an error if there are replicated resources. func (r *Reflector) Stop() error { - r.mu.Lock() - defer r.mu.Unlock() - - klog.Infof("[%v] Stopping reflection towards remote cluster", r.remoteClusterID) - - for gvr := range r.resources { - if err := r.stopForResource(gvr); err != nil { - return err - } - } + return r.stop(false) +} - r.cancel() - return nil +// StopForce stops the reflection towards the remote cluster, ignoring any replicated resource. This means that if replication is not +// restored, then there might be some orphan replicated resource in the remote cluster. +func (r *Reflector) StopForce() error { + return r.stop(true) } // ResourceStarted returns whether the reflection for the given resource has been started. @@ -163,16 +169,32 @@ func (r *Reflector) StartForResource(ctx context.Context, resource *resources.Re }() } -// StopForResource stops the reflection of the given resource, and removes the replicated objects. +// StopForResource stops the reflection of the given resource. It fails if there are replicated objects. func (r *Reflector) StopForResource(resource *resources.Resource) error { r.mu.Lock() defer r.mu.Unlock() - return r.stopForResource(resource.GroupVersionResource) + return r.stopForResource(resource.GroupVersionResource, false) } -// stopForResource stops the reflection of the given resource, and removes the replicated objects. -func (r *Reflector) stopForResource(gvr schema.GroupVersionResource) error { +func (r *Reflector) stop(skipResourcePresenceCheck bool) error { + r.mu.Lock() + defer r.mu.Unlock() + + klog.Infof("[%v] Stopping reflection towards remote cluster", r.remoteClusterID) + + for gvr := range r.resources { + if err := r.stopForResource(gvr, skipResourcePresenceCheck); err != nil { + return err + } + } + + r.cancel() + return nil +} + +// stopForResource stops the reflection of the given resource. Unless skipResourcePresenceCheck is false, it fails if there are replicated objects. +func (r *Reflector) stopForResource(gvr schema.GroupVersionResource, skipResourcePresenceCheck bool) error { rs, found := r.resources[gvr] if !found { // This resource was already stopped, just return @@ -181,24 +203,26 @@ func (r *Reflector) stopForResource(gvr schema.GroupVersionResource) error { klog.Infof("[%v] Stopping reflection of %v", r.remoteClusterID, gvr) - // Check if any object is still present in the local or in the remote cluster - for key, lister := range map[string]cache.GenericNamespaceLister{"local": rs.local, "remote": rs.remote} { - objects, err := lister.List(labels.Everything()) - - if key == "remote" && apierrors.IsForbidden(err) { - // The remote cluster has probably removed the necessary permissions to operate on reflected resources, let's ignore the error - klog.Infof("[%v] Cannot list %v objects in the remote cluster (permission removed by provider).", r.remoteClusterID, gvr) - continue - } - - if err != nil { - klog.Errorf("[%v] Failed to stop reflection of %v: %v", r.remoteClusterID, gvr, err) - return err - } - - if len(objects) > 0 { - klog.Errorf("[%v] Cannot stop reflection of %v, since %v objects are still present", r.remoteClusterID, gvr, key) - return fmt.Errorf("%v %v still present for cluster %v", key, gvr, r.remoteClusterID) + if !skipResourcePresenceCheck { + // Check if any object is still present in the local or in the remote cluster + for key, lister := range map[string]cache.GenericNamespaceLister{"local": rs.local, "remote": rs.remote} { + objects, err := lister.List(labels.Everything()) + + if key == "remote" && apierrors.IsForbidden(err) { + // The remote cluster has probably removed the necessary permissions to operate on reflected resources, let's ignore the error + klog.Infof("[%v] Cannot list %v objects in the remote cluster (permission removed by provider).", r.remoteClusterID, gvr) + continue + } + + if err != nil { + klog.Errorf("[%v] Failed to stop reflection of %v: %v", r.remoteClusterID, gvr, err) + return err + } + + if len(objects) > 0 { + klog.Errorf("[%v] Cannot stop reflection of %v, since %v objects are still present", r.remoteClusterID, gvr, key) + return fmt.Errorf("%v %v still present for cluster %v", key, gvr, r.remoteClusterID) + } } } diff --git a/internal/crdReplicator/reflection/reflector_test.go b/internal/crdReplicator/reflection/reflector_test.go index a351639089..f65197c737 100644 --- a/internal/crdReplicator/reflection/reflector_test.go +++ b/internal/crdReplicator/reflection/reflector_test.go @@ -72,7 +72,7 @@ var _ = Describe("Reflector tests", func() { manager = NewManager(local, localClusterID, workers, 0) manager.Start(ctx, []resources.Resource{res}) - reflector = manager.NewForRemote(remote, remoteClusterID, localNamespace, remoteNamespace) + reflector = manager.NewForRemote(remote, remoteClusterID, localNamespace, remoteNamespace, "") }) AfterEach(func() { cancel() }) @@ -157,7 +157,7 @@ var _ = Describe("Reflector tests", func() { Eventually(func() bool { return reflector.resources[gvr].initialized }).Should(BeTrue()) }) - JustBeforeEach(func() { err = reflector.stopForResource(gvr) }) + JustBeforeEach(func() { err = reflector.stopForResource(gvr, false) }) When("no object is present", func() { It("should succeed", func() { Expect(err).ToNot(HaveOccurred()) }) diff --git a/pkg/identityManager/client.go b/pkg/identityManager/client.go index dc8eea34f8..453782853e 100644 --- a/pkg/identityManager/client.go +++ b/pkg/identityManager/client.go @@ -40,16 +40,7 @@ func (certManager *identityManager) GetConfig(remoteCluster liqov1beta1.ClusterI return nil, err } - cnf, err := kubeconfig.BuildConfigFromSecret(secret) - if err != nil { - return nil, err - } - - if certManager.isAwsIdentity(secret) { - return certManager.mutateIAMConfig(secret, remoteCluster, cnf) - } - - return cnf, nil + return certManager.GetConfigFromSecret(remoteCluster, secret) } func (certManager *identityManager) GetSecretNamespacedName(remoteCluster liqov1beta1.ClusterID, diff --git a/pkg/liqo-controller-manager/authentication/csr.go b/pkg/liqo-controller-manager/authentication/csr.go index e0710139c3..d20d4b59a0 100644 --- a/pkg/liqo-controller-manager/authentication/csr.go +++ b/pkg/liqo-controller-manager/authentication/csr.go @@ -90,7 +90,7 @@ func IsControlPlaneUser(groups []string) bool { // CheckCSRForControlPlane checks a CSR for a control plane. func CheckCSRForControlPlane(csr, publicKey []byte, remoteClusterID liqov1beta1.ClusterID) error { - return checkCSR(csr, publicKey, + return checkCSR(csr, publicKey, true, func(x509Csr *x509.CertificateRequest) error { if x509Csr.Subject.CommonName != CommonNameControlPlaneCSR(remoteClusterID) { return fmt.Errorf("invalid common name") @@ -106,8 +106,8 @@ func CheckCSRForControlPlane(csr, publicKey []byte, remoteClusterID liqov1beta1. } // CheckCSRForResourceSlice checks a CSR for a resource slice. -func CheckCSRForResourceSlice(publicKey []byte, resourceSlice *authv1beta1.ResourceSlice) error { - return checkCSR(resourceSlice.Spec.CSR, publicKey, +func CheckCSRForResourceSlice(publicKey []byte, resourceSlice *authv1beta1.ResourceSlice, checkPublicKey bool) error { + return checkCSR(resourceSlice.Spec.CSR, publicKey, checkPublicKey, func(x509Csr *x509.CertificateRequest) error { if x509Csr.Subject.CommonName != CommonNameResourceSliceCSR(resourceSlice) { return fmt.Errorf("invalid common name") @@ -122,7 +122,7 @@ func CheckCSRForResourceSlice(publicKey []byte, resourceSlice *authv1beta1.Resou }) } -func checkCSR(csr, publicKey []byte, commonName, organization CSRChecker) error { +func checkCSR(csr, publicKey []byte, checkPublicKey bool, commonName, organization CSRChecker) error { pemCsr, rst := pem.Decode(csr) if pemCsr == nil || len(rst) != 0 { return fmt.Errorf("invalid CSR") @@ -141,18 +141,27 @@ func checkCSR(csr, publicKey []byte, commonName, organization CSRChecker) error return err } - // if the pub key is 0-terminated, drop it - if publicKey[len(publicKey)-1] == 0 { - publicKey = publicKey[:len(publicKey)-1] - } - - switch crtKey := x509Csr.PublicKey.(type) { - case ed25519.PublicKey: - if !bytes.Equal(crtKey, publicKey) { + if checkPublicKey { + // Check the length of the public key and return an error if invalid + if len(publicKey) == 0 { return fmt.Errorf("invalid public key") } - default: - return fmt.Errorf("invalid public key type %T", crtKey) + + // if the pub key is 0-terminated, drop it + if publicKey[len(publicKey)-1] == 0 { + publicKey = publicKey[:len(publicKey)-1] + } + + // Check that the public key used the expected algorithm and verify that the CSR has been + // signed with the key provided by the peer at peering time. + switch crtKey := x509Csr.PublicKey.(type) { + case ed25519.PublicKey: + if !bytes.Equal(crtKey, publicKey) { + return fmt.Errorf("invalid public key") + } + default: + return fmt.Errorf("invalid public key type %T", crtKey) + } } return nil diff --git a/pkg/liqo-controller-manager/authentication/remoteresourceslice-controller/remoteresourceslice_controller.go b/pkg/liqo-controller-manager/authentication/remoteresourceslice-controller/remoteresourceslice_controller.go index a738b8691c..eae98501c9 100644 --- a/pkg/liqo-controller-manager/authentication/remoteresourceslice-controller/remoteresourceslice_controller.go +++ b/pkg/liqo-controller-manager/authentication/remoteresourceslice-controller/remoteresourceslice_controller.go @@ -17,9 +17,10 @@ package remoteresourceslicecontroller import ( "context" "fmt" + "strings" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -98,7 +99,7 @@ type RemoteResourceSliceReconciler struct { func (r *RemoteResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { var resourceSlice authv1beta1.ResourceSlice if err = r.Get(ctx, req.NamespacedName, &resourceSlice); err != nil { - if errors.IsNotFound(err) { + if kerrors.IsNotFound(err) { klog.V(4).Infof("resourceSlice %q not found", req.NamespacedName) return ctrl.Result{}, nil } @@ -135,6 +136,19 @@ func (r *RemoteResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl. } }() + // Make sure that the resource slice has been created in the tentant namespace dedicated to the current consumer + err = validateRSNamespace(ctx, r.Client, req.Namespace, string(*resourceSlice.Spec.ConsumerClusterID)) + switch { + case kerrors.IsBadRequest(err): + klog.Errorf("Invalid ResourceSlice %q provided: %s", req.NamespacedName, err) + r.eventRecorder.Event(&resourceSlice, corev1.EventTypeWarning, "Invalid ResourceSlice", err.Error()) + denyAuthentication(&resourceSlice, r.eventRecorder) + return ctrl.Result{}, nil + case err != nil: + klog.Errorf("unable to get tenant Namespace of ResourceSlice %q: %v", req.NamespacedName, err) + return ctrl.Result{}, err + } + // Handle the ResourceSlice authentication status if err = r.handleAuthenticationStatus(ctx, &resourceSlice, tenant); err != nil { return ctrl.Result{}, err @@ -151,7 +165,8 @@ func (r *RemoteResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl. func (r *RemoteResourceSliceReconciler) handleAuthenticationStatus(ctx context.Context, resourceSlice *authv1beta1.ResourceSlice, tenant *authv1beta1.Tenant) error { // check that the CSR is valid - if err := authentication.CheckCSRForResourceSlice(tenant.Spec.PublicKey, resourceSlice); err != nil { + shouldCheckPublicKey := authv1beta1.GetAuthzPolicyValue(tenant.Spec.AuthzPolicy) != authv1beta1.TolerateNoHandshake + if err := authentication.CheckCSRForResourceSlice(tenant.Spec.PublicKey, resourceSlice, shouldCheckPublicKey); err != nil { klog.Errorf("Invalid CSR for the ResourceSlice %q: %s", client.ObjectKeyFromObject(resourceSlice), err) r.eventRecorder.Event(resourceSlice, corev1.EventTypeWarning, "InvalidCSR", err.Error()) denyAuthentication(resourceSlice, r.eventRecorder) @@ -256,7 +271,11 @@ func (r *RemoteResourceSliceReconciler) SetupWithManager(mgr ctrl.Manager) error } return ctrl.NewControllerManagedBy(mgr).Named(consts.CtrlResourceSliceRemote). - For(&authv1beta1.ResourceSlice{}, builder.WithPredicates(predicate.And(remoteResSliceFilter, withCSR()))). + For( + &authv1beta1.ResourceSlice{}, + // With GenerationChangedPredicate we prevent to reconcile multiple times when the status of the resource changes + builder.WithPredicates(predicate.And(remoteResSliceFilter, withCSR(), predicate.GenerationChangedPredicate{})), + ). Watches(&authv1beta1.Tenant{}, handler.EnqueueRequestsFromMapFunc(r.resourceSlicesEnquer())). Complete(r) } @@ -388,3 +407,23 @@ func isInResourceClasses(resourceSlice *authv1beta1.ResourceSlice, classes ...au } return false } + +// validateRSNamespace makes sure that the ResourceSlice has been created in the tenant namespace dedicated to the consumer cluster. +func validateRSNamespace(ctx context.Context, c client.Client, namespace, consumerClusterID string) error { + var tenantNamespace corev1.Namespace + if err := c.Get(ctx, types.NamespacedName{Name: namespace}, &tenantNamespace); err != nil { + return err + } + + if tenantLabel, labelPresent := tenantNamespace.Labels[consts.TenantNamespaceLabel]; !labelPresent || strings.EqualFold(tenantLabel, "false") { + return kerrors.NewBadRequest("A ResourceSlice cannot be created in a namespace different than the Tenant namespace") + } + + if clusterIDLabel, labelPresent := tenantNamespace.Labels[consts.RemoteClusterID]; !labelPresent || clusterIDLabel != consumerClusterID { + return kerrors.NewBadRequest( + fmt.Sprintf("ResourceSlice belonging to %q has been created in the Tenant namespace of %q", consumerClusterID, clusterIDLabel), + ) + } + + return nil +} diff --git a/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go b/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go index 772a4e549a..8450f06ac9 100644 --- a/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go +++ b/pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go @@ -134,38 +134,41 @@ func (r *TenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res clusterID := tenant.Spec.ClusterID - // get the nonce for the tenant - - nonceSecret, err := getters.GetNonceSecretByClusterID(ctx, r.Client, clusterID) - if err != nil { - klog.Errorf("Unable to get the nonce for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "NonceNotFound", err.Error()) - return ctrl.Result{}, err - } + // If no handshake is tolerated, then do not perform the checks on the exchanged keys. + if authv1beta1.GetAuthzPolicyValue(tenant.Spec.AuthzPolicy) != authv1beta1.TolerateNoHandshake { + // get the nonce for the tenant + + nonceSecret, err := getters.GetNonceSecretByClusterID(ctx, r.Client, clusterID) + if err != nil { + klog.Errorf("Unable to get the nonce for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "NonceNotFound", err.Error()) + return ctrl.Result{}, err + } - nonce, err := authgetters.GetNonceFromSecret(nonceSecret) - if err != nil { - klog.Errorf("Unable to get the nonce for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "NonceNotFound", err.Error()) - return ctrl.Result{}, err - } + nonce, err := authgetters.GetNonceFromSecret(nonceSecret) + if err != nil { + klog.Errorf("Unable to get the nonce for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "NonceNotFound", err.Error()) + return ctrl.Result{}, err + } - // check the signature + // check the signature - if !authentication.VerifyNonce(ed25519.PublicKey(tenant.Spec.PublicKey), nonce, tenant.Spec.Signature) { - err = fmt.Errorf("signature verification failed for Tenant %q", req.Name) - klog.Error(err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error()) - return ctrl.Result{}, nil - } + if !authentication.VerifyNonce(ed25519.PublicKey(tenant.Spec.PublicKey), nonce, tenant.Spec.Signature) { + err = fmt.Errorf("signature verification failed for Tenant %q", req.Name) + klog.Error(err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error()) + return ctrl.Result{}, nil + } - // check that the CSR is created with the same public key + // check that the CSR is created with the same public key - if err = authentication.CheckCSRForControlPlane( - tenant.Spec.CSR, tenant.Spec.PublicKey, tenant.Spec.ClusterID); err != nil { - klog.Errorf("Invalid CSR for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "InvalidCSR", err.Error()) - return ctrl.Result{}, nil + if err = authentication.CheckCSRForControlPlane( + tenant.Spec.CSR, tenant.Spec.PublicKey, tenant.Spec.ClusterID); err != nil { + klog.Errorf("Invalid CSR for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "InvalidCSR", err.Error()) + return ctrl.Result{}, nil + } } // create the tenant namespace @@ -192,44 +195,47 @@ func (r *TenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res } }() - // create the CSR and forge the AuthParams - - authParams, err := r.IdentityProvider.ForgeAuthParams(ctx, &identitymanager.SigningRequestOptions{ - Cluster: tenant.Spec.ClusterID, - TenantNamespace: tenant.Status.TenantNamespace, - IdentityType: authv1beta1.ControlPlaneIdentityType, - Name: tenant.Name, - SigningRequest: tenant.Spec.CSR, - - APIServerAddressOverride: r.APIServerAddressOverride, - CAOverride: r.CAOverride, - TrustedCA: r.TrustedCA, - ProxyURL: tenant.Spec.ProxyURL, - }) - if err != nil { - klog.Errorf("Unable to forge the AuthParams for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "AuthParamsFailed", err.Error()) - return ctrl.Result{}, err - } + // If no handshake is performed, then the user is charge of creating the authentication params and bind the right permissions. + if authv1beta1.GetAuthzPolicyValue(tenant.Spec.AuthzPolicy) != authv1beta1.TolerateNoHandshake { + // create the CSR and forge the AuthParams + + authParams, err := r.IdentityProvider.ForgeAuthParams(ctx, &identitymanager.SigningRequestOptions{ + Cluster: tenant.Spec.ClusterID, + TenantNamespace: tenant.Status.TenantNamespace, + IdentityType: authv1beta1.ControlPlaneIdentityType, + Name: tenant.Name, + SigningRequest: tenant.Spec.CSR, + + APIServerAddressOverride: r.APIServerAddressOverride, + CAOverride: r.CAOverride, + TrustedCA: r.TrustedCA, + ProxyURL: tenant.Spec.ProxyURL, + }) + if err != nil { + klog.Errorf("Unable to forge the AuthParams for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "AuthParamsFailed", err.Error()) + return ctrl.Result{}, err + } - tenant.Status.AuthParams = authParams + tenant.Status.AuthParams = authParams - // bind permissions + // bind permissions - _, err = r.NamespaceManager.BindClusterRoles(ctx, tenant.Spec.ClusterID, - tenant, r.tenantClusterRoles...) - if err != nil { - klog.Errorf("Unable to bind the ClusterRoles for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "ClusterRolesBindingFailed", err.Error()) - return ctrl.Result{}, err - } + _, err = r.NamespaceManager.BindClusterRoles(ctx, tenant.Spec.ClusterID, + tenant, r.tenantClusterRoles...) + if err != nil { + klog.Errorf("Unable to bind the ClusterRoles for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "ClusterRolesBindingFailed", err.Error()) + return ctrl.Result{}, err + } - _, err = r.NamespaceManager.BindClusterRolesClusterWide(ctx, tenant.Spec.ClusterID, - tenant, r.tenantClusterRolesClusterWide...) - if err != nil { - klog.Errorf("Unable to bind the ClusterRolesClusterWide for the Tenant %q: %s", req.Name, err) - r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "ClusterRolesClusterWideBindingFailed", err.Error()) - return ctrl.Result{}, err + _, err = r.NamespaceManager.BindClusterRolesClusterWide(ctx, tenant.Spec.ClusterID, + tenant, r.tenantClusterRolesClusterWide...) + if err != nil { + klog.Errorf("Unable to bind the ClusterRolesClusterWide for the Tenant %q: %s", req.Name, err) + r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "ClusterRolesClusterWideBindingFailed", err.Error()) + return ctrl.Result{}, err + } } return ctrl.Result{}, nil diff --git a/pkg/liqo-controller-manager/core/foreigncluster-controller/status.go b/pkg/liqo-controller-manager/core/foreigncluster-controller/status.go index 53142813d8..c1c4fde08f 100644 --- a/pkg/liqo-controller-manager/core/foreigncluster-controller/status.go +++ b/pkg/liqo-controller-manager/core/foreigncluster-controller/status.go @@ -24,6 +24,7 @@ import ( "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" liqov1beta1 "github.com/liqotech/liqo/apis/core/v1beta1" networkingv1beta1 "github.com/liqotech/liqo/apis/networking/v1beta1" offloadingv1beta1 "github.com/liqotech/liqo/apis/offloading/v1beta1" @@ -190,7 +191,9 @@ func (r *ForeignClusterReconciler) handleAuthenticationModuleStatus(ctx context. fc.Status.TenantNamespace.Local = tenant.Status.TenantNamespace } - if tenant.Status.AuthParams == nil || tenant.Status.TenantNamespace == "" { + // Define the status of the authentication module based on whether the keys exchange has been performed. + expectKeysExchange := authv1beta1.GetAuthzPolicyValue(tenant.Spec.AuthzPolicy) != authv1beta1.TolerateNoHandshake + if expectKeysExchange && tenant.Status.AuthParams == nil || tenant.Status.TenantNamespace == "" { fcutils.EnsureModuleCondition(&fc.Status.Modules.Authentication, liqov1beta1.AuthTenantStatusCondition, liqov1beta1.ConditionStatusNotReady, tenantNotReadyReason, tenantNotReadyMessage) diff --git a/pkg/liqoctl/info/peer/auth.go b/pkg/liqoctl/info/peer/auth.go index 259e0ad34f..c8a0294110 100644 --- a/pkg/liqoctl/info/peer/auth.go +++ b/pkg/liqoctl/info/peer/auth.go @@ -74,18 +74,20 @@ func (ac *AuthChecker) Collect(ctx context.Context, options info.Options) { authStatus := Auth{} ac.collectStatusInfo(clusterID, options.ClustersInfo, &authStatus) - if options.ClustersInfo[clusterID].Status.Role == liqov1beta1.ProviderRole { - if err := ac.collectAPIAddress(ctx, options.CRClient, clusterID, &authStatus); err != nil { - ac.AddCollectionError(fmt.Errorf("unable to get API server address of cluster %q: %w", clusterID, err)) + if authStatus.Status != common.ModuleDisabled { + if options.ClustersInfo[clusterID].Status.Role == liqov1beta1.ProviderRole { + if err := ac.collectAPIAddress(ctx, options.CRClient, clusterID, &authStatus); err != nil { + ac.AddCollectionError(fmt.Errorf("unable to get API server address of cluster %q: %w", clusterID, err)) + } } - } - // Get the ResourceSlices related to the given remote clusterID - resSlices, err := getters.ListResourceSlicesByClusterID(ctx, options.CRClient, clusterID) - if err != nil { - ac.AddCollectionError(fmt.Errorf("unable to get ResourceSlices of cluster %q: %w", clusterID, err)) - } else { - ac.collectResourceSlices(resSlices, &authStatus) + // Get the ResourceSlices related to the given remote clusterID + resSlices, err := getters.ListResourceSlicesByClusterID(ctx, options.CRClient, clusterID) + if err != nil { + ac.AddCollectionError(fmt.Errorf("unable to get ResourceSlices of cluster %q: %w", clusterID, err)) + } else { + ac.collectResourceSlices(resSlices, &authStatus) + } } ac.data[clusterID] = authStatus