...

Source file src/edge-infra.dev/pkg/edge/iam/ctl/encryptionctl/encryption_secret_controller.go

Documentation: edge-infra.dev/pkg/edge/iam/ctl/encryptionctl

     1  /*
     2  Copyright 2022.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package encryptionctl
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"sigs.k8s.io/controller-runtime/pkg/event"
    27  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    33  
    34  	edgeConditions "edge-infra.dev/pkg/k8s/runtime/conditions"
    35  
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	logger "sigs.k8s.io/controller-runtime/pkg/log"
    38  
    39  	"k8s.io/apimachinery/pkg/types"
    40  
    41  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    42  
    43  	"github.com/go-logr/logr"
    44  
    45  	api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    46  	"edge-infra.dev/pkg/edge/iam/storage/database"
    47  )
    48  
    49  // Future work: provider gets deleted? -> provider's status will not have db version since db version is set in here
    50  // EncryptionSecretReconciler watches for new encryption secrets created by providerctl
    51  type EncryptionSecretReconciler struct {
    52  	client.Client
    53  	Scheme *runtime.Scheme
    54  
    55  	Name string
    56  
    57  	// kubebuilder default metrics
    58  	Metrics metrics.Metrics
    59  }
    60  
    61  const encryptionKeyPrefix = "id-encryption-key-"
    62  
    63  // +kubebuilder:rbac:groups=iam.edge-infra.dev,resources=providers,verbs=get;list;watch
    64  // +kubebuilder:rbac:groups="",resources=pods;serviceaccounts;secrets;configmaps;services;namespaces,verbs=create;get;list;update;patch;watch;delete
    65  // +kubebuilder:rbac:groups="external-secrets.io",resources=externalsecrets,verbs=get;watch;create;patch;update;list
    66  // +kubebuilder:rbac:groups="monitoring.coreos.com",resources=servicemonitors,verbs=create;get;list;update;patch;watch;delete
    67  
    68  // trigger the controller to reconcile if:
    69  // - a new encryption key is created or deleted
    70  func secretReconcilerPredicate() predicate.Predicate {
    71  	return predicate.Funcs{
    72  		// The key point to note is that a finalizer causes delete on the object to become an update
    73  		// to set deletion timestamp. Presence of deletion timestamp on the object indicates that it is being deleted.
    74  		UpdateFunc: func(e event.UpdateEvent) bool {
    75  			s, ok := e.ObjectNew.(*corev1.Secret)
    76  			if ok && e.ObjectNew.GetNamespace() == "edge-iam" &&
    77  				strings.HasPrefix(e.ObjectNew.GetName(), encryptionKeyPrefix) &&
    78  				!s.ObjectMeta.DeletionTimestamp.IsZero() {
    79  				return true
    80  			}
    81  			return false
    82  		},
    83  		// are we creating a secret?
    84  		// is it in the edge-iam ns?
    85  		// does it start with "id-encryption-key-"?
    86  		CreateFunc: func(e event.CreateEvent) bool {
    87  			_, ok := e.Object.(*corev1.Secret)
    88  			if ok && e.Object.GetNamespace() == "edge-iam" &&
    89  				strings.HasPrefix(e.Object.GetName(), encryptionKeyPrefix) {
    90  				return true
    91  			}
    92  			return false
    93  		},
    94  		DeleteFunc: func(_ event.DeleteEvent) bool {
    95  			return false
    96  		},
    97  	}
    98  }
    99  
   100  func (r *EncryptionSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   101  	log := logger.FromContext(ctx)
   102  	finalizer := "finalizers.edge.ncr.com/encryption"
   103  
   104  	// get the provider, bc we need the spec.encryption.Version
   105  	var provider = api.Provider{}
   106  	err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: "provider"}, &provider)
   107  	if err != nil {
   108  		log.Error(err, "Unable to get provider crd")
   109  		return ctrl.Result{}, err
   110  	}
   111  
   112  	// grab the secret corresponding to the request
   113  	var secret = corev1.Secret{}
   114  	err = r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, &secret)
   115  	if err != nil {
   116  		log.Error(err, "Unable to get encryption secret", "secret", req.Name)
   117  		return ctrl.Result{}, err
   118  	}
   119  
   120  	// if provider.spec.encryption.version = provider.status.version
   121  	// we can return without reconciling as db's are up to date with provider's spec version
   122  	statusVersion, upToDate := CheckStatusVersion(provider)
   123  	if upToDate && secret.ObjectMeta.DeletionTimestamp.IsZero() {
   124  		log.Info("Databases are already up to date with provider's spec.encryption.version. Returning without reconciling encryption secret controller.")
   125  		return ctrl.Result{}, nil
   126  	}
   127  
   128  	// if secret is being deleted, do we need to decrypt the databases?
   129  	if !secret.ObjectMeta.DeletionTimestamp.IsZero() { //nolint
   130  		if controllerutil.ContainsFinalizer(&secret, finalizer) {
   131  			if provider.Spec.Encryption.Version == "" {
   132  				err := r.DecryptDatabases(ctx, secret)
   133  				if err != nil {
   134  					log.Error(err, "Unable to decrypt databases")
   135  					return ctrl.Result{}, err
   136  				}
   137  			}
   138  
   139  			// if we have finalizer, remove it
   140  			controllerutil.RemoveFinalizer(&secret, finalizer)
   141  			if err := r.Update(ctx, &secret); err != nil {
   142  				log.Error(err, "Unable update secret to remove finalizer for deletion")
   143  				return ctrl.Result{}, err
   144  			}
   145  
   146  			log.Info("Removed Finalizer on secret", "secret", secret.Name)
   147  		}
   148  		// if secret is not being deleted, we have created a secret for encryption or rotation reconciliation
   149  	} else {
   150  		controllerutil.AddFinalizer(&secret, finalizer)
   151  		err = r.Client.Update(ctx, &secret)
   152  		if err != nil {
   153  			log.Error(err, "Unable to update secret to include encryption finalizer")
   154  			return ctrl.Result{}, err
   155  		}
   156  
   157  		// rotate or encrypt databases
   158  		reconcileErr := r.reconcile(ctx, req, statusVersion, provider)
   159  		if reconcileErr != nil {
   160  			log.Error(reconcileErr, "reconciled with error")
   161  			return ctrl.Result{RequeueAfter: 5 * time.Second}, reconcileErr
   162  		}
   163  	}
   164  
   165  	// if we have reconciled with no errors, let's update the provider crd's Status to reflect new version
   166  	if err := r.updateStatus(ctx, provider); err != nil {
   167  		log.Error(err, "unable to update status")
   168  		return ctrl.Result{Requeue: true}, err
   169  	}
   170  
   171  	return ctrl.Result{}, nil
   172  }
   173  
   174  // SetupWithManager sets up the controller with the Manager.
   175  func (r *EncryptionSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
   176  	return ctrl.NewControllerManagedBy(mgr).
   177  		For(&corev1.Secret{}).
   178  		WithEventFilter(secretReconcilerPredicate()).
   179  		Complete(r)
   180  }
   181  
   182  // reconcile checks if we need to update the databases, either encrypting or rotating
   183  func (r *EncryptionSecretReconciler) reconcile(ctx context.Context, req ctrl.Request, statusVersion string, provider api.Provider) error {
   184  	log := logger.FromContext(ctx)
   185  	version := provider.Spec.Encryption.Version
   186  
   187  	// generating the new encryption secret's name
   188  	newSecretName := encryptionKeyPrefix + version
   189  
   190  	// grab the secret
   191  	var newSecret = corev1.Secret{}
   192  	err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: newSecretName}, &newSecret)
   193  	if err != nil {
   194  		log.Error(err, "Unable to get new encryption secret", newSecretName)
   195  		return err
   196  	}
   197  
   198  	newKey := newSecret.Data["key"]
   199  
   200  	// Encrypt: if no encryption version found in the provider's status, let's try to encrypt the databases
   201  	if statusVersion == "" {
   202  		err = encryptDatabases(ctx, log, newKey)
   203  		if err != nil {
   204  			log.Error(err, "Unable to encrypt databases")
   205  			return err
   206  		}
   207  	}
   208  
   209  	// Rotate: if we find two encryption secrets in the ns, let's try to rotate the databases using the two keys
   210  	oldKey, rotate, err := r.rotateKeys(ctx, newSecretName)
   211  	if err != nil {
   212  		log.Error(err, "Unable to determine if we need to rotate databases")
   213  		return err
   214  	}
   215  
   216  	if rotate {
   217  		err = updateDatabases(ctx, log, oldKey, newKey)
   218  		if err != nil {
   219  			log.Error(err, "Failed to update dabases with new encryption key")
   220  			return err
   221  		}
   222  	}
   223  
   224  	log.Info("Databases successfully updated")
   225  	return nil
   226  }
   227  
   228  // update the databases to use the new secret key
   229  func updateDatabases(ctx context.Context, log logr.Logger, oldKey []byte, newKey []byte) error {
   230  	s, err := database.NewProviderStore(log)
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	// update couch db
   236  	err = s.RotateCouchEncryptionKey(ctx, oldKey, newKey)
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	log.Info("Successfully updated couchdb to use the new encryption key")
   242  	return nil
   243  }
   244  
   245  // for going from unencrypted -> encrypted
   246  func encryptDatabases(ctx context.Context, log logr.Logger, newKey []byte) error {
   247  	s, err := database.NewProviderStore(log)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	// update couch db
   253  	err = s.EncryptCouchDB(ctx, newKey)
   254  	if err != nil {
   255  		log.Error(err, "Unable to encrypt couchdb")
   256  		return err
   257  	}
   258  
   259  	log.Info("Successfully updated couchdb to be encrypted and use the new encryption key")
   260  	return nil
   261  }
   262  
   263  // update the provider's status to show the current encryptipon key version the databases are using
   264  func (r *EncryptionSecretReconciler) updateStatus(ctx context.Context, provider api.Provider) error {
   265  	statusMessage := "successfully updated databases to version: " + provider.Spec.Encryption.Version
   266  
   267  	var p = api.Provider{}
   268  	if err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: "provider"}, &p); err != nil {
   269  		return err
   270  	}
   271  	gen := p.GetGeneration()
   272  	condition := metav1.Condition{
   273  		Type:               "DatabaseUpdated",
   274  		Status:             metav1.ConditionTrue,
   275  		Reason:             "EncryptionRotationSucceeded",
   276  		Message:            statusMessage,
   277  		ObservedGeneration: gen,
   278  		LastTransitionTime: metav1.Now(),
   279  	}
   280  
   281  	patch := client.MergeFrom(p.DeepCopy())
   282  
   283  	edgeConditions.Set(&p, &condition)
   284  
   285  	return r.Status().Patch(ctx, &p, patch)
   286  }
   287  
   288  // CheckStatusVersion returns the provider status version (aka, the databases), and sees if it matches Spec.Encryption.Version
   289  func CheckStatusVersion(provider api.Provider) (string, bool) {
   290  	statusVersion := ""
   291  	prefix := "successfully updated databases to version: "
   292  
   293  	for i := range provider.Status.Conditions {
   294  		if provider.Status.Conditions[i].Reason == "EncryptionRotationSucceeded" {
   295  			// grab the version from the end of the message
   296  			statusVersion = strings.TrimSpace(provider.Status.Conditions[i].Message[len(prefix):])
   297  		}
   298  	}
   299  
   300  	// does the version in the EncryptionRotationSucceeded status match the provider's latest version
   301  	return statusVersion, statusVersion == provider.Spec.Encryption.Version
   302  }
   303  
   304  // rotateKeys checks if we need to rotate the databases
   305  func (r *EncryptionSecretReconciler) rotateKeys(ctx context.Context, newSecretName string) ([]byte, bool, error) {
   306  	log := logger.FromContext(ctx)
   307  
   308  	// get secrets in the namespace
   309  	secretList := &corev1.SecretList{}
   310  	if err := r.Client.List(ctx, secretList, &client.ListOptions{Namespace: "edge-iam"}); err != nil {
   311  		log.Error(err, "Couldn't list secrets in the edge-iam namespace")
   312  		return []byte(""), false, err
   313  	}
   314  
   315  	// search for secret starting with 'id-encryption-key' that is NOT the new secret
   316  	for _, secret := range secretList.Items {
   317  		if strings.HasPrefix(secret.Name, "id-encryption-key") && secret.Name != newSecretName {
   318  			var oldSecret = corev1.Secret{}
   319  			err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: secret.Name}, &oldSecret)
   320  			if err != nil {
   321  				log.Error(err, fmt.Sprintf("Unable to get old secret %s", secret.Name))
   322  				return []byte(""), false, err
   323  			}
   324  
   325  			oldKey := oldSecret.Data["key"]
   326  			log.Info("Found key to rotate", "key", secret.Name)
   327  			return oldKey, true, nil
   328  		}
   329  	}
   330  
   331  	return []byte(""), false, nil
   332  }
   333  
   334  // update the databases to be decrypted
   335  func decryptDatabases(ctx context.Context, oldKey []byte) error {
   336  	log := logger.FromContext(ctx)
   337  	s, err := database.NewProviderStore(log)
   338  	if err != nil {
   339  		return err
   340  	}
   341  	// update couch db
   342  	err = s.DecryptCouchDB(ctx, oldKey)
   343  	if err != nil {
   344  		log.Info("Unable to decrypt couch database", err)
   345  		return err
   346  	}
   347  	log.Info("Successfully decrypted couchdb")
   348  	return nil
   349  }
   350  
   351  func (r *EncryptionSecretReconciler) DecryptDatabases(ctx context.Context, secret corev1.Secret) error {
   352  	log := logger.FromContext(ctx)
   353  
   354  	key := secret.Data["key"]
   355  
   356  	err := decryptDatabases(ctx, key)
   357  	if err != nil {
   358  		log.Info("Error reverting databases to be unencrypted.")
   359  		return err
   360  	}
   361  
   362  	log.Info("Successfully decrypted databases.")
   363  	return nil
   364  }
   365  

View as plain text