open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/encryptionkeys/encryptionkeys_controller.go (about)

     1  // Copyright (c) 2022 Red Hat, Inc.
     2  // Copyright Contributors to the Open Cluster Management project
     3  
     4  package encryptionkeys
     5  
     6  import (
     7  	"context"
     8  	"crypto/aes"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	corev1 "k8s.io/api/core/v1"
    14  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/types"
    18  	ctrl "sigs.k8s.io/controller-runtime"
    19  	"sigs.k8s.io/controller-runtime/pkg/client"
    20  	"sigs.k8s.io/controller-runtime/pkg/controller"
    21  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    22  
    23  	policyv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
    24  	"open-cluster-management.io/governance-policy-propagator/controllers/common"
    25  	"open-cluster-management.io/governance-policy-propagator/controllers/propagator"
    26  )
    27  
    28  const (
    29  	ControllerName = "policy-encryption-keys"
    30  	// This is used for when an administrator prefers to manually generate the encryption keys
    31  	// instead of letting the Policy Propagator handle it.
    32  	DisableRotationAnnotation = "policy.open-cluster-management.io/disable-rotation"
    33  )
    34  
    35  var (
    36  	log                       = ctrl.Log.WithName(ControllerName)
    37  	errLastRotationParseError = fmt.Errorf(`failed to parse the "%s" annotation`, propagator.LastRotatedAnnotation)
    38  )
    39  
    40  // SetupWithManager sets up the controller with the Manager.
    41  func (r *EncryptionKeysReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles uint) error {
    42  	return ctrl.NewControllerManagedBy(mgr).
    43  		// The work queue prevents the same item being reconciled concurrently:
    44  		// https://github.com/kubernetes-sigs/controller-runtime/issues/1416#issuecomment-899833144
    45  		WithOptions(controller.Options{MaxConcurrentReconciles: int(maxConcurrentReconciles)}).
    46  		Named(ControllerName).
    47  		For(&corev1.Secret{}).
    48  		Complete(r)
    49  }
    50  
    51  // blank assignment to verify that EncryptionKeysReconciler implements reconcile.Reconciler
    52  var _ reconcile.Reconciler = &EncryptionKeysReconciler{}
    53  
    54  // EncryptionKeysReconciler is responsible for rotating the AES encryption key in the "policy-encryption-key" Secrets
    55  // for all managed clusters.
    56  type EncryptionKeysReconciler struct { //nolint:golint,revive
    57  	client.Client
    58  	KeyRotationDays uint
    59  	Scheme          *runtime.Scheme
    60  }
    61  
    62  //+kubebuilder:rbac:groups=policy.open-cluster-management.io,resources=policies,verbs=get;list;patch
    63  //+kubebuilder:rbac:groups=core,resources=secrets,verbs=create
    64  //+kubebuilder:rbac:groups=core,resources=secrets,resourceNames=policy-encryption-key,verbs=get;list;update;watch
    65  
    66  // Reconcile watches all "policy-encryption-key" Secrets on the Hub cluster. This periodically rotates the keys
    67  // and resolves invalid modifications made to the Secret.
    68  func (r *EncryptionKeysReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
    69  	log := log.WithValues("secretNamespace", request.Namespace, "secret", request.Name)
    70  	log.Info("Reconciling a Secret")
    71  
    72  	clusterName := request.Namespace
    73  
    74  	// The cache configuration of SelectorsByObject should prevent this from happening, but add this as a precaution.
    75  	if request.Name != propagator.EncryptionKeySecret {
    76  		log.Info("Got a reconciliation request for an unexpected Secret. This should have been filtered out.")
    77  
    78  		return reconcile.Result{}, nil
    79  	}
    80  
    81  	secret := &corev1.Secret{}
    82  
    83  	err := r.Get(ctx, request.NamespacedName, secret)
    84  	if err != nil {
    85  		if k8serrors.IsNotFound(err) {
    86  			log.V(2).Info("The Secret was not found on the server. Doing nothing.")
    87  
    88  			return ctrl.Result{}, nil
    89  		}
    90  
    91  		log.Error(err, "Failed to get the Secret from the server. Will retry the reconcile request.")
    92  
    93  		return ctrl.Result{}, err
    94  	}
    95  
    96  	annotations := secret.GetAnnotations()
    97  	if strings.EqualFold(annotations[DisableRotationAnnotation], "true") {
    98  		log.Info(
    99  			"Encountered an encryption key Secret with key rotation disabled. Will trigger a policy template update.",
   100  			"annotation", DisableRotationAnnotation,
   101  			"value", annotations[DisableRotationAnnotation],
   102  		)
   103  
   104  		// In case the key was manually rotated, trigger a template update
   105  		r.triggerTemplateUpdate(ctx, clusterName, secret)
   106  
   107  		return reconcile.Result{}, nil
   108  	}
   109  
   110  	lastRotatedTS := annotations[propagator.LastRotatedAnnotation]
   111  	log = log.WithValues("annotation", propagator.LastRotatedAnnotation, "value", lastRotatedTS)
   112  
   113  	var nextRotation time.Duration
   114  
   115  	if lastRotatedTS == "" {
   116  		log.Info("The annotation is not set. Will rotate the key now.")
   117  
   118  		nextRotation = 0
   119  	} else {
   120  		nextRotation, err = r.getNextRotationFromNow(secret)
   121  		if err != nil {
   122  			log.Error(err, "The annotation cannot be parsed. Will rotate the key now.")
   123  			nextRotation = 0
   124  		}
   125  	}
   126  
   127  	_, err = aes.NewCipher(secret.Data["key"])
   128  	if err != nil {
   129  		log.Error(err, "The encryption key in the Secret is invalid. Will rotate the key now.")
   130  
   131  		nextRotation = 0
   132  		// Set this to a null value so the bad key doesn't get stored as the previous key
   133  		secret.Data["key"] = []byte{}
   134  	}
   135  
   136  	if nextRotation > 0 {
   137  		// previousKey only needs to be checked if there won't be a rotation since it would get overwritten anyways
   138  		if len(secret.Data["previousKey"]) > 0 {
   139  			// If previousKey is invalid, it'll cause go-template-utils to fail on the managed cluster, so remove it if
   140  			// a user changed this accidentally
   141  			_, err = aes.NewCipher(secret.Data["previousKey"])
   142  			if err != nil {
   143  				log.Info("The previous encryption key in the Secret is invalid. Will remove it.")
   144  
   145  				secret.Data["previousKey"] = []byte{}
   146  
   147  				err := r.Update(ctx, secret)
   148  				if err != nil {
   149  					log.Error(err, "Failed to update the Secret. Will retry the request.")
   150  
   151  					return reconcile.Result{}, err
   152  				}
   153  			}
   154  		}
   155  
   156  		log.V(2).Info("The key is not yet ready to be rotated")
   157  
   158  		// Requeueing the same object multiple times is safe as the queue will drop any scheduled
   159  		// requeues in favor of this one
   160  		// https://github.com/kubernetes-sigs/controller-runtime/blob/7ba3e559790c5e3543002a0d9670b4cef3ccf743/pkg/internal/controller/controller.go#L323-L324
   161  		return reconcile.Result{RequeueAfter: nextRotation}, nil
   162  	}
   163  
   164  	log.V(1).Info("Rotating the encryption key")
   165  
   166  	err = r.rotateKey(ctx, secret)
   167  	if err != nil {
   168  		log.Error(err, "Failed to rotate the encryption key. Will retry the request.")
   169  
   170  		return reconcile.Result{}, err
   171  	}
   172  
   173  	r.triggerTemplateUpdate(ctx, clusterName, secret)
   174  
   175  	// The error is ignored since this can't fail since the annotation value was just set to a valid value
   176  	nextRotation, _ = r.getNextRotationFromNow(secret)
   177  
   178  	log.Info("Rotated the encryption key successfully", "nextRotation", nextRotation)
   179  
   180  	return reconcile.Result{RequeueAfter: nextRotation}, nil
   181  }
   182  
   183  // getNextRotationFromNow will return the duration from now until the next key rotation. An error is
   184  // returned if the last rotated annotation cannot be parsed.
   185  func (r *EncryptionKeysReconciler) getNextRotationFromNow(secret *corev1.Secret) (time.Duration, error) {
   186  	annotations := secret.GetAnnotations()
   187  	lastRotatedTS := annotations[propagator.LastRotatedAnnotation]
   188  
   189  	lastRotated, err := time.Parse(time.RFC3339, lastRotatedTS)
   190  	if err != nil {
   191  		return 0, fmt.Errorf(`%w with value "%s": %w`, errLastRotationParseError, lastRotatedTS, err)
   192  	}
   193  
   194  	nextRotation := lastRotated.Add(time.Hour * 24 * time.Duration(r.KeyRotationDays))
   195  
   196  	return time.Until(nextRotation), nil
   197  }
   198  
   199  // rotateKey will generate a new encryption key to replace the "key" field/key on the Secret. The
   200  // old key is stored as the "previousKey" field/key on the Secret. The Secret is then updated
   201  // with the Kubernetes API server. An error is returned if the key can't be generated or the
   202  // update on the API servier fails.
   203  func (r *EncryptionKeysReconciler) rotateKey(ctx context.Context, secret *corev1.Secret) error {
   204  	newKey, err := propagator.GenerateEncryptionKey()
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	secret.Data["previousKey"] = secret.Data["key"]
   210  	secret.Data["key"] = newKey
   211  
   212  	annotations := secret.GetAnnotations()
   213  	if annotations == nil {
   214  		annotations = map[string]string{}
   215  	}
   216  
   217  	lastRotatedTS := time.Now().UTC().Format(time.RFC3339)
   218  	annotations[propagator.LastRotatedAnnotation] = lastRotatedTS
   219  	secret.SetAnnotations(annotations)
   220  
   221  	return r.Update(ctx, secret)
   222  }
   223  
   224  // triggerTemplateUpdate finds all the policies that this managed cluster uses that use encryption.
   225  // It then updates those root policies with the trigger-update annotation to cause the policies to
   226  // be reprocessed with the new key.
   227  func (r *EncryptionKeysReconciler) triggerTemplateUpdate(
   228  	ctx context.Context, clusterName string, secret *corev1.Secret,
   229  ) {
   230  	log.Info(
   231  		"Triggering template updates on all the managed cluster policies that use encryption", "cluster", clusterName,
   232  	)
   233  
   234  	policies := policyv1.PolicyList{}
   235  	// Get all the policies in the cluster namespace
   236  	err := r.List(ctx, &policies, client.InNamespace(clusterName))
   237  	if err != nil {
   238  		log.Error(err, "Failed to trigger all the policies to be reprocessed after the key rotation")
   239  
   240  		return
   241  	}
   242  
   243  	// Setting this value to something unique for this key rotation ensures the annotation will be updated to
   244  	// a new value and thus trigger an update
   245  	value := fmt.Sprintf("rotate-key-%s-%s", clusterName, secret.Annotations[propagator.LastRotatedAnnotation])
   246  	patch := []byte(`{"metadata":{"annotations":{"` + propagator.TriggerUpdateAnnotation + `":"` + value + `"}}}`)
   247  
   248  	for _, policy := range policies.Items {
   249  		// If the policy does not have the initialization vector annotation, then encryption is not
   250  		// used and thus doesn't need to be reprocessed
   251  		if _, ok := policy.Annotations[propagator.IVAnnotation]; !ok {
   252  			continue
   253  		}
   254  
   255  		// Find the root policy to patch with the annotation
   256  		rootPlcName := policy.GetLabels()[common.RootPolicyLabel]
   257  		if rootPlcName == "" {
   258  			log.Info(
   259  				"The replicated policy does not have the root policy label set",
   260  				"policy", policy.ObjectMeta.Name,
   261  				"label", common.RootPolicyLabel,
   262  			)
   263  
   264  			continue
   265  		}
   266  
   267  		name, namespace, err := common.ParseRootPolicyLabel(rootPlcName)
   268  		if err != nil {
   269  			log.Error(err, "Unable to parse name and namespace of root policy, ignoring this replicated policy",
   270  				"rootPlcName", rootPlcName)
   271  
   272  			continue
   273  		}
   274  
   275  		rootPolicy := &policyv1.Policy{
   276  			ObjectMeta: metav1.ObjectMeta{
   277  				Namespace: namespace,
   278  				Name:      name,
   279  			},
   280  		}
   281  
   282  		err = r.Patch(ctx, rootPolicy, client.RawPatch(types.MergePatchType, patch))
   283  		if err != nil {
   284  			log.Error(
   285  				err,
   286  				"Failed to trigger the policy to be reprocessed after the key rotation",
   287  				"policyName",
   288  				rootPlcName,
   289  			)
   290  		}
   291  	}
   292  }