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 }