package bannerctl import ( "context" "encoding/json" "fmt" "strconv" "strings" "time" kms "cloud.google.com/go/kms/apiv1" "cloud.google.com/go/kms/apiv1/kmspb" "github.com/go-logr/logr" "github.com/google/uuid" "google.golang.org/protobuf/types/known/fieldmaskpb" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/edge/api/services/channels" bannerAPI "edge-infra.dev/pkg/edge/apis/banner/v1alpha1" "edge-infra.dev/pkg/edge/edgeencrypt" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr" ) type EncryptionKeyManagementReconciler struct { client.Client Log logr.Logger Conditions reconcile.Conditions ForemanProjectID string GCPRegion string SecretManager secretManager KmsClient *kms.KeyManagementClient kmsKey edgeencrypt.KmsKey ChannelService channels.Service IntervalTime time.Duration RequeueTime time.Duration ResourceTimeout time.Duration } func (r *EncryptionKeyManagementReconciler) SetupWithManager(mgr ctrl.Manager) error { r.kmsKey = edgeencrypt.KmsKey{ProjectID: r.ForemanProjectID, Location: r.GCPRegion} return ctrl.NewControllerManagedBy(mgr). For(&bannerAPI.Banner{}). Complete(r) } func (r *EncryptionKeyManagementReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, recErr error) { //nolint:dupl banner := &bannerAPI.Banner{} if err := r.Get(ctx, req.NamespacedName, banner); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } log := ctrl.Log.WithValues("banner", banner.Name) // Get the list of channels for the banner bcs, err := getBannerChannels(ctx, r.ChannelService, banner.Name) if err != nil { return ctrl.Result{RequeueAfter: r.IntervalTime}, nil } if len(bcs) == 0 { return ctrl.Result{RequeueAfter: r.RequeueTime}, nil } recErr = r.reconcile(ctx, banner, bcs) if recErr != nil { log.Error(recErr, "failed to reconcile") return ctrl.Result{RequeueAfter: r.IntervalTime}, nil } return ctrl.Result{RequeueAfter: r.RequeueTime}, nil } func (r *EncryptionKeyManagementReconciler) reconcile(ctx context.Context, banner *bannerAPI.Banner, chs []channels.BannerChannel) recerr.Error { secretClient, err := r.SecretManager.NewWithOptions(ctx, banner.Spec.GCP.ProjectID) if err != nil { return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason) } //defer secretClient.Close() bannerEdgeID, err := uuid.Parse(banner.Name) if err != nil { return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason) } // TODO add step to disable old `keys` after past expired keys and buffer time for _, channel := range chs { // check if the key version should be rotated if channel.ShouldBumpKeyVersion() { ver, err := r.createNewKeyVersion(ctx, banner, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID)) if err != nil { return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason) } err = r.rotateKey(ctx, secretClient, bannerEdgeID, banner, channel, ver) if err != nil { return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason) } } if _, validKey := channel.LatestKeyVersion(); !validKey { continue } for _, k := range channel.KeyVersions { if k.IsExpired() { key := r.kmsKey.KeyPath(banner.Name, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID), fmt.Sprintf("%d", k.Version)) req := &kmspb.UpdateCryptoKeyVersionRequest{ CryptoKeyVersion: &kmspb.CryptoKeyVersion{ Name: key, State: kmspb.CryptoKeyVersion_DISABLED, }, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, } if _, err = r.KmsClient.UpdateCryptoKeyVersion(ctx, req); err != nil { return recerr.New(fmt.Errorf("fail to disable key: %s, %w", key, err), bannerAPI.PlatformSecretsCreationFailedReason) } r.Log.Info("encryption key disabled", "key", key) _, err = r.ChannelService.DeleteChannelKeyVersion(ctx, k.ID) if err != nil { return recerr.New(fmt.Errorf("fail to delete channel key version: %s, %w", k.ID, err), bannerAPI.PlatformSecretsCreationFailedReason) } } } } return nil } func (r *EncryptionKeyManagementReconciler) createNewKeyVersion(ctx context.Context, banner *bannerAPI.Banner, channel string) (int, error) { req := &kmspb.CreateCryptoKeyVersionRequest{Parent: r.kmsKey.Key(banner.Name, channel)} result, err := r.KmsClient.CreateCryptoKeyVersion(ctx, req) if err != nil { return 0, fmt.Errorf("failed to create key version: %w", err) } // `projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*`. token := strings.Split(result.Name, "/") version := token[len(token)-1] versionInt, err := strconv.Atoi(version) if err != nil { return 0, fmt.Errorf("failed to convert version to int: %w", err) } return versionInt, nil } func (r *EncryptionKeyManagementReconciler) rotateKey(ctx context.Context, secretClient secretManagerClient, bannerEdgeID uuid.UUID, banner *bannerAPI.Banner, channel channels.BannerChannel, version int) error { ckv := channels.ChannelKeyVersion{ ChannelID: channel.ID, BannerEdgeID: bannerEdgeID, Version: version, SecretManagerLink: secretManagerLink(banner.Spec.GCP.ProjectID, fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID)), } _, err := r.ChannelService.CreateChannelKeyVersion(ctx, ckv) if err != nil { return fmt.Errorf("failed to create channel key version: %w", err) } key := r.kmsKey.KeyPath(banner.Name, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID), fmt.Sprintf("%d", version)) pubKey, err := getPublicKeyWithRetry(ctx, r.KmsClient, key, r.ResourceTimeout) if err != nil { return fmt.Errorf("failed to get public key from kms to rotate: %w", err) } pk, err := edgeencrypt.NewPublicKey(pubKey.Pem, fmt.Sprintf("%d", version)) if err != nil { return fmt.Errorf("failed to create public key: %w", err) } data, err := json.Marshal(pk) if err != nil { return fmt.Errorf("failed to marshal public key: %w", err) } err = saveToSecretManager(ctx, secretClient, fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID), data, version, map[string]string{"channel": channel.Name}) if err != nil { return fmt.Errorf("failed to save rotated public key to secret manager: %w", err) } return nil }