/* Copyright 2022. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package encryptionctl import ( "context" "fmt" "strings" "time" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" edgeConditions "edge-infra.dev/pkg/k8s/runtime/conditions" "sigs.k8s.io/controller-runtime/pkg/client" logger "sigs.k8s.io/controller-runtime/pkg/log" "k8s.io/apimachinery/pkg/types" "edge-infra.dev/pkg/k8s/runtime/controller/metrics" "github.com/go-logr/logr" api "edge-infra.dev/pkg/edge/iam/api/v1alpha1" "edge-infra.dev/pkg/edge/iam/storage/database" ) // Future work: provider gets deleted? -> provider's status will not have db version since db version is set in here // EncryptionSecretReconciler watches for new encryption secrets created by providerctl type EncryptionSecretReconciler struct { client.Client Scheme *runtime.Scheme Name string // kubebuilder default metrics Metrics metrics.Metrics } const encryptionKeyPrefix = "id-encryption-key-" // +kubebuilder:rbac:groups=iam.edge-infra.dev,resources=providers,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=pods;serviceaccounts;secrets;configmaps;services;namespaces,verbs=create;get;list;update;patch;watch;delete // +kubebuilder:rbac:groups="external-secrets.io",resources=externalsecrets,verbs=get;watch;create;patch;update;list // +kubebuilder:rbac:groups="monitoring.coreos.com",resources=servicemonitors,verbs=create;get;list;update;patch;watch;delete // trigger the controller to reconcile if: // - a new encryption key is created or deleted func secretReconcilerPredicate() predicate.Predicate { return predicate.Funcs{ // The key point to note is that a finalizer causes delete on the object to become an update // to set deletion timestamp. Presence of deletion timestamp on the object indicates that it is being deleted. UpdateFunc: func(e event.UpdateEvent) bool { s, ok := e.ObjectNew.(*corev1.Secret) if ok && e.ObjectNew.GetNamespace() == "edge-iam" && strings.HasPrefix(e.ObjectNew.GetName(), encryptionKeyPrefix) && !s.ObjectMeta.DeletionTimestamp.IsZero() { return true } return false }, // are we creating a secret? // is it in the edge-iam ns? // does it start with "id-encryption-key-"? CreateFunc: func(e event.CreateEvent) bool { _, ok := e.Object.(*corev1.Secret) if ok && e.Object.GetNamespace() == "edge-iam" && strings.HasPrefix(e.Object.GetName(), encryptionKeyPrefix) { return true } return false }, DeleteFunc: func(_ event.DeleteEvent) bool { return false }, } } func (r *EncryptionSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logger.FromContext(ctx) finalizer := "finalizers.edge.ncr.com/encryption" // get the provider, bc we need the spec.encryption.Version var provider = api.Provider{} err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: "provider"}, &provider) if err != nil { log.Error(err, "Unable to get provider crd") return ctrl.Result{}, err } // grab the secret corresponding to the request var secret = corev1.Secret{} err = r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, &secret) if err != nil { log.Error(err, "Unable to get encryption secret", "secret", req.Name) return ctrl.Result{}, err } // if provider.spec.encryption.version = provider.status.version // we can return without reconciling as db's are up to date with provider's spec version statusVersion, upToDate := CheckStatusVersion(provider) if upToDate && secret.ObjectMeta.DeletionTimestamp.IsZero() { log.Info("Databases are already up to date with provider's spec.encryption.version. Returning without reconciling encryption secret controller.") return ctrl.Result{}, nil } // if secret is being deleted, do we need to decrypt the databases? if !secret.ObjectMeta.DeletionTimestamp.IsZero() { //nolint if controllerutil.ContainsFinalizer(&secret, finalizer) { if provider.Spec.Encryption.Version == "" { err := r.DecryptDatabases(ctx, secret) if err != nil { log.Error(err, "Unable to decrypt databases") return ctrl.Result{}, err } } // if we have finalizer, remove it controllerutil.RemoveFinalizer(&secret, finalizer) if err := r.Update(ctx, &secret); err != nil { log.Error(err, "Unable update secret to remove finalizer for deletion") return ctrl.Result{}, err } log.Info("Removed Finalizer on secret", "secret", secret.Name) } // if secret is not being deleted, we have created a secret for encryption or rotation reconciliation } else { controllerutil.AddFinalizer(&secret, finalizer) err = r.Client.Update(ctx, &secret) if err != nil { log.Error(err, "Unable to update secret to include encryption finalizer") return ctrl.Result{}, err } // rotate or encrypt databases reconcileErr := r.reconcile(ctx, req, statusVersion, provider) if reconcileErr != nil { log.Error(reconcileErr, "reconciled with error") return ctrl.Result{RequeueAfter: 5 * time.Second}, reconcileErr } } // if we have reconciled with no errors, let's update the provider crd's Status to reflect new version if err := r.updateStatus(ctx, provider); err != nil { log.Error(err, "unable to update status") return ctrl.Result{Requeue: true}, err } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *EncryptionSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Secret{}). WithEventFilter(secretReconcilerPredicate()). Complete(r) } // reconcile checks if we need to update the databases, either encrypting or rotating func (r *EncryptionSecretReconciler) reconcile(ctx context.Context, req ctrl.Request, statusVersion string, provider api.Provider) error { log := logger.FromContext(ctx) version := provider.Spec.Encryption.Version // generating the new encryption secret's name newSecretName := encryptionKeyPrefix + version // grab the secret var newSecret = corev1.Secret{} err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: newSecretName}, &newSecret) if err != nil { log.Error(err, "Unable to get new encryption secret", newSecretName) return err } newKey := newSecret.Data["key"] // Encrypt: if no encryption version found in the provider's status, let's try to encrypt the databases if statusVersion == "" { err = encryptDatabases(ctx, log, newKey) if err != nil { log.Error(err, "Unable to encrypt databases") return err } } // Rotate: if we find two encryption secrets in the ns, let's try to rotate the databases using the two keys oldKey, rotate, err := r.rotateKeys(ctx, newSecretName) if err != nil { log.Error(err, "Unable to determine if we need to rotate databases") return err } if rotate { err = updateDatabases(ctx, log, oldKey, newKey) if err != nil { log.Error(err, "Failed to update dabases with new encryption key") return err } } log.Info("Databases successfully updated") return nil } // update the databases to use the new secret key func updateDatabases(ctx context.Context, log logr.Logger, oldKey []byte, newKey []byte) error { s, err := database.NewProviderStore(log) if err != nil { return err } // update couch db err = s.RotateCouchEncryptionKey(ctx, oldKey, newKey) if err != nil { return err } log.Info("Successfully updated couchdb to use the new encryption key") return nil } // for going from unencrypted -> encrypted func encryptDatabases(ctx context.Context, log logr.Logger, newKey []byte) error { s, err := database.NewProviderStore(log) if err != nil { return err } // update couch db err = s.EncryptCouchDB(ctx, newKey) if err != nil { log.Error(err, "Unable to encrypt couchdb") return err } log.Info("Successfully updated couchdb to be encrypted and use the new encryption key") return nil } // update the provider's status to show the current encryptipon key version the databases are using func (r *EncryptionSecretReconciler) updateStatus(ctx context.Context, provider api.Provider) error { statusMessage := "successfully updated databases to version: " + provider.Spec.Encryption.Version var p = api.Provider{} if err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: "provider"}, &p); err != nil { return err } gen := p.GetGeneration() condition := metav1.Condition{ Type: "DatabaseUpdated", Status: metav1.ConditionTrue, Reason: "EncryptionRotationSucceeded", Message: statusMessage, ObservedGeneration: gen, LastTransitionTime: metav1.Now(), } patch := client.MergeFrom(p.DeepCopy()) edgeConditions.Set(&p, &condition) return r.Status().Patch(ctx, &p, patch) } // CheckStatusVersion returns the provider status version (aka, the databases), and sees if it matches Spec.Encryption.Version func CheckStatusVersion(provider api.Provider) (string, bool) { statusVersion := "" prefix := "successfully updated databases to version: " for i := range provider.Status.Conditions { if provider.Status.Conditions[i].Reason == "EncryptionRotationSucceeded" { // grab the version from the end of the message statusVersion = strings.TrimSpace(provider.Status.Conditions[i].Message[len(prefix):]) } } // does the version in the EncryptionRotationSucceeded status match the provider's latest version return statusVersion, statusVersion == provider.Spec.Encryption.Version } // rotateKeys checks if we need to rotate the databases func (r *EncryptionSecretReconciler) rotateKeys(ctx context.Context, newSecretName string) ([]byte, bool, error) { log := logger.FromContext(ctx) // get secrets in the namespace secretList := &corev1.SecretList{} if err := r.Client.List(ctx, secretList, &client.ListOptions{Namespace: "edge-iam"}); err != nil { log.Error(err, "Couldn't list secrets in the edge-iam namespace") return []byte(""), false, err } // search for secret starting with 'id-encryption-key' that is NOT the new secret for _, secret := range secretList.Items { if strings.HasPrefix(secret.Name, "id-encryption-key") && secret.Name != newSecretName { var oldSecret = corev1.Secret{} err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: secret.Name}, &oldSecret) if err != nil { log.Error(err, fmt.Sprintf("Unable to get old secret %s", secret.Name)) return []byte(""), false, err } oldKey := oldSecret.Data["key"] log.Info("Found key to rotate", "key", secret.Name) return oldKey, true, nil } } return []byte(""), false, nil } // update the databases to be decrypted func decryptDatabases(ctx context.Context, oldKey []byte) error { log := logger.FromContext(ctx) s, err := database.NewProviderStore(log) if err != nil { return err } // update couch db err = s.DecryptCouchDB(ctx, oldKey) if err != nil { log.Info("Unable to decrypt couch database", err) return err } log.Info("Successfully decrypted couchdb") return nil } func (r *EncryptionSecretReconciler) DecryptDatabases(ctx context.Context, secret corev1.Secret) error { log := logger.FromContext(ctx) key := secret.Data["key"] err := decryptDatabases(ctx, key) if err != nil { log.Info("Error reverting databases to be unencrypted.") return err } log.Info("Successfully decrypted databases.") return nil }