...

Source file src/edge-infra.dev/pkg/edge/iam/ctl/providerctl/provider_encryption_controller.go

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

     1  package providerctl
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"time"
     7  
     8  	goext "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
     9  	"github.com/fluxcd/pkg/ssa"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    12  	logger "sigs.k8s.io/controller-runtime/pkg/log"
    13  
    14  	"sigs.k8s.io/controller-runtime/pkg/client"
    15  
    16  	unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
    17  
    18  	"k8s.io/apimachinery/pkg/api/errors"
    19  	"k8s.io/apimachinery/pkg/types"
    20  
    21  	"edge-infra.dev/pkg/edge/constants"
    22  
    23  	api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    24  
    25  	"edge-infra.dev/pkg/edge/iam/config"
    26  )
    27  
    28  func (r *ProviderReconciler) reconcileEncryptionKeyRotation(ctx context.Context, provider api.Provider) (api.Provider, error) {
    29  	log := logger.FromContext(ctx)
    30  
    31  	// if we do not have an encryption version, do not encrypt
    32  	if provider.Spec.Encryption.Version == "" { //nolint
    33  		// try to get any encryption-key-[] external secrets in the edge-iam ns
    34  		var externalSecrets = &goext.ExternalSecretList{}
    35  		err := r.Client.List(ctx, externalSecrets, &client.ListOptions{Namespace: "edge-iam"})
    36  		if err != nil {
    37  			return provider, err
    38  		}
    39  
    40  		if len(externalSecrets.Items) > 0 {
    41  			// do they start with the encryption prefix
    42  			for _, s := range externalSecrets.Items {
    43  				if strings.Contains(s.Name, EncryptionKeySecretPrefix) {
    44  					log.Info("Found encryption external secrets that should be deleted as encryption is no longer enabled")
    45  
    46  					// grab the external secret
    47  					extSec, err := r.checkExternalSecrets(ctx, s.Name)
    48  					if err != nil {
    49  						log.Error(err, "Unable to check if external secret exists")
    50  						return provider, err
    51  					}
    52  					_, err = r.ResourceManager.Delete(ctx, extSec, ssa.DefaultDeleteOptions())
    53  					if err != nil {
    54  						log.Error(err, "Unable to delete old external secret")
    55  						return provider, err
    56  					}
    57  					log.Info("Deleted old external secret", "secret", s.Name)
    58  				}
    59  			}
    60  			// remove the encryption status from the provider
    61  			provider = RemoveEncryptionStatus(provider)
    62  		}
    63  		log.Info("Encryption is not enabled")
    64  		return provider, nil
    65  	} else { // nolint
    66  		log.Info("Encryption is enabled", "version", provider.Spec.Encryption.Version)
    67  	}
    68  
    69  	// create the k8s secret name
    70  	secretName := EncryptionKeySecretPrefix + provider.Spec.Encryption.Version
    71  
    72  	// check if we've already created the external secret before
    73  	extSec, err := r.checkExternalSecrets(ctx, secretName)
    74  	if err != nil {
    75  		log.Error(err, "Unable to check if external secret exists")
    76  		return provider, err
    77  	}
    78  
    79  	// if it doesn't exist, create
    80  	if extSec == nil {
    81  		// generate name of encryption key
    82  		keyName := "id-encryption-key-" + config.ClusterID()
    83  
    84  		// create external secret as unstructured
    85  		extSec, err := CreateEncryptionExternalSecret(provider.Spec.Encryption.Version, keyName, secretName)
    86  		if err != nil {
    87  			return provider, err
    88  		}
    89  
    90  		// apply external secret
    91  		_, err = r.ResourceManager.Apply(ctx, extSec, ssa.ApplyOptions{Force: true})
    92  		if err != nil {
    93  			return provider, err
    94  		}
    95  		log.Info("Created external secret for encryption", "secret", secretName)
    96  	}
    97  
    98  	// if provider's db status version = provider's encryption.version
    99  	// we can delete the old external secret (if found)
   100  	_, matches := DoesStatusMatchSpecVersion(provider)
   101  	if matches {
   102  		err = r.removeOldExternalSecret(ctx, secretName)
   103  		if err != nil {
   104  			log.Error(err, "Error trying to remove an old external secret")
   105  			return provider, err
   106  		}
   107  	}
   108  	return provider, nil
   109  }
   110  
   111  // check if we need to create a new external secret object by trying to grab it
   112  func (r *ProviderReconciler) checkExternalSecrets(ctx context.Context, secretName string) (*unstructured.Unstructured, error) {
   113  	var externalSecret = &goext.ExternalSecret{}
   114  	err := r.Get(ctx, types.NamespacedName{Namespace: "edge-iam", Name: secretName}, externalSecret)
   115  	if err != nil {
   116  		if errors.IsNotFound(err) {
   117  			return nil, nil
   118  		}
   119  		return nil, err
   120  	}
   121  
   122  	// convert to unstructured
   123  	uobj, err := unstructuredutil.ToUnstructured(externalSecret)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	return uobj, nil
   129  }
   130  
   131  // Creating the following as an unstructured obj:
   132  //
   133  //	apiVersion: external-secrets.io/v1beta1
   134  //	kind: ExternalSecret
   135  //	metadata:
   136  //	name: id-encryption-key-[version]
   137  //	namespace: edge-iam
   138  //	labels:
   139  //		platform.edge.ncr.com/component: edge-iam
   140  //	spec:
   141  //		dataFrom:
   142  //		- extract:
   143  //			key: [key] // typically id-encryption-key-[clusterid]
   144  //			version: version
   145  //		refreshInterval: 1m
   146  //		secretStoreRef:
   147  //			name: gcp-provider
   148  //			kind: ClusterSecretStore
   149  //		target:
   150  //			name: id-encryption-key-[version]
   151  //			creationPolicy: Owner
   152  func CreateEncryptionExternalSecret(version string, keyName string, secretName string) (*unstructured.Unstructured, error) {
   153  	extSec := &goext.ExternalSecret{
   154  		TypeMeta: metav1.TypeMeta{
   155  			APIVersion: goext.ExtSecretGroupVersionKind.GroupVersion().String(),
   156  			Kind:       goext.ExtSecretGroupVersionKind.Kind,
   157  		},
   158  		ObjectMeta: metav1.ObjectMeta{
   159  			Name:      secretName,
   160  			Namespace: "edge-iam",
   161  			Labels: map[string]string{
   162  				constants.PlatformComponent: "edge-iam",
   163  			},
   164  		},
   165  		Spec: goext.ExternalSecretSpec{
   166  			DataFrom: []goext.ExternalSecretDataFromRemoteRef{
   167  				{
   168  					Extract: &goext.ExternalSecretDataRemoteRef{
   169  						Key:     keyName,
   170  						Version: version,
   171  					},
   172  				},
   173  			},
   174  			RefreshInterval: &metav1.Duration{
   175  				Duration: time.Minute,
   176  			},
   177  			SecretStoreRef: goext.SecretStoreRef{
   178  				Name: "gcp-provider",
   179  				Kind: "ClusterSecretStore",
   180  			},
   181  			Target: goext.ExternalSecretTarget{
   182  				Name:           secretName,
   183  				CreationPolicy: goext.CreatePolicyOwner,
   184  			},
   185  		},
   186  	}
   187  
   188  	uobj, err := unstructuredutil.ToUnstructured(extSec)
   189  	if err != nil {
   190  		return uobj, err
   191  	}
   192  
   193  	return uobj, nil
   194  }
   195  
   196  // DoesStatusMatchSpecVersion checks if the provider status version (aka, the databases) matches Spec.Encryption.Version
   197  func DoesStatusMatchSpecVersion(provider api.Provider) (string, bool) {
   198  	dbVersion := ""
   199  	prefix := "successfully updated databases to version: "
   200  
   201  	for i := range provider.Status.Conditions {
   202  		if provider.Status.Conditions[i].Reason == "EncryptionRotationSucceeded" {
   203  			// grab the version from the end of the message
   204  			dbVersion = strings.TrimSpace(provider.Status.Conditions[i].Message[len(prefix):])
   205  		}
   206  	}
   207  
   208  	// does the version in the EncryptionRotationSucceeded status match the provider's latest version
   209  	return dbVersion, dbVersion == provider.Spec.Encryption.Version
   210  }
   211  
   212  // removeOldExternalSecret removes the old external secret from the ns
   213  func (r *ProviderReconciler) removeOldExternalSecret(ctx context.Context, newSecretName string) error {
   214  	log := logger.FromContext(ctx)
   215  
   216  	// get external secrets in the namespace
   217  	secretList := &goext.ExternalSecretList{}
   218  	if err := r.Client.List(ctx, secretList, &client.ListOptions{Namespace: "edge-iam"}); err != nil {
   219  		log.Error(err, "Couldn't list external secrets in the edge-iam namespace")
   220  		return err
   221  	}
   222  
   223  	// search for external secret starting with 'id-encryption-key' that is NOT the new secret
   224  	for _, secret := range secretList.Items {
   225  		if strings.HasPrefix(secret.Name, "id-encryption-key") && secret.Name != newSecretName {
   226  			// grab the old external secret
   227  			extSec, err := r.checkExternalSecrets(ctx, secret.Name)
   228  			if err != nil {
   229  				log.Error(err, "Unable to check if external secret exists")
   230  				return err
   231  			}
   232  			_, err = r.ResourceManager.Delete(ctx, extSec, ssa.DefaultDeleteOptions())
   233  			if err != nil {
   234  				log.Error(err, "Unable to delete old external secret")
   235  				return err
   236  			}
   237  			log.Info("Deleted old external secret", "secret", secret.Name)
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  func RemoveEncryptionStatus(provider api.Provider) api.Provider {
   244  	providerStatus := provider.Status.Conditions
   245  
   246  	for i, condition := range providerStatus {
   247  		if condition.Reason == "EncryptionRotationSucceeded" {
   248  			// remove the condition from the list
   249  			provider.Status.Conditions = append(providerStatus[:i], providerStatus[i+1:]...)
   250  		}
   251  	}
   252  
   253  	return provider
   254  }
   255  

View as plain text