...

Source file src/edge-infra.dev/pkg/edge/controllers/bannerctl/encryption_key_management.go

Documentation: edge-infra.dev/pkg/edge/controllers/bannerctl

     1  package bannerctl
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	kms "cloud.google.com/go/kms/apiv1"
    12  	"cloud.google.com/go/kms/apiv1/kmspb"
    13  	"github.com/go-logr/logr"
    14  	"github.com/google/uuid"
    15  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    16  	ctrl "sigs.k8s.io/controller-runtime"
    17  	"sigs.k8s.io/controller-runtime/pkg/client"
    18  
    19  	"edge-infra.dev/pkg/edge/api/services/channels"
    20  	bannerAPI "edge-infra.dev/pkg/edge/apis/banner/v1alpha1"
    21  	"edge-infra.dev/pkg/edge/edgeencrypt"
    22  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    23  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
    24  )
    25  
    26  type EncryptionKeyManagementReconciler struct {
    27  	client.Client
    28  	Log              logr.Logger
    29  	Conditions       reconcile.Conditions
    30  	ForemanProjectID string
    31  	GCPRegion        string
    32  	SecretManager    secretManager
    33  	KmsClient        *kms.KeyManagementClient
    34  	kmsKey           edgeencrypt.KmsKey
    35  	ChannelService   channels.Service
    36  
    37  	IntervalTime    time.Duration
    38  	RequeueTime     time.Duration
    39  	ResourceTimeout time.Duration
    40  }
    41  
    42  func (r *EncryptionKeyManagementReconciler) SetupWithManager(mgr ctrl.Manager) error {
    43  	r.kmsKey = edgeencrypt.KmsKey{ProjectID: r.ForemanProjectID, Location: r.GCPRegion}
    44  	return ctrl.NewControllerManagedBy(mgr).
    45  		For(&bannerAPI.Banner{}).
    46  		Complete(r)
    47  }
    48  
    49  func (r *EncryptionKeyManagementReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, recErr error) { //nolint:dupl
    50  	banner := &bannerAPI.Banner{}
    51  	if err := r.Get(ctx, req.NamespacedName, banner); err != nil {
    52  		return ctrl.Result{}, client.IgnoreNotFound(err)
    53  	}
    54  	log := ctrl.Log.WithValues("banner", banner.Name)
    55  
    56  	// Get the list of channels for the banner
    57  	bcs, err := getBannerChannels(ctx, r.ChannelService, banner.Name)
    58  	if err != nil {
    59  		return ctrl.Result{RequeueAfter: r.IntervalTime}, nil
    60  	}
    61  	if len(bcs) == 0 {
    62  		return ctrl.Result{RequeueAfter: r.RequeueTime}, nil
    63  	}
    64  
    65  	recErr = r.reconcile(ctx, banner, bcs)
    66  	if recErr != nil {
    67  		log.Error(recErr, "failed to reconcile")
    68  		return ctrl.Result{RequeueAfter: r.IntervalTime}, nil
    69  	}
    70  
    71  	return ctrl.Result{RequeueAfter: r.RequeueTime}, nil
    72  }
    73  
    74  func (r *EncryptionKeyManagementReconciler) reconcile(ctx context.Context, banner *bannerAPI.Banner, chs []channels.BannerChannel) recerr.Error {
    75  	secretClient, err := r.SecretManager.NewWithOptions(ctx, banner.Spec.GCP.ProjectID)
    76  	if err != nil {
    77  		return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
    78  	}
    79  	//defer secretClient.Close()
    80  
    81  	bannerEdgeID, err := uuid.Parse(banner.Name)
    82  	if err != nil {
    83  		return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
    84  	}
    85  
    86  	// TODO add step to disable old `keys` after past expired keys and buffer time
    87  
    88  	for _, channel := range chs {
    89  		// check if the key version should be rotated
    90  		if channel.ShouldBumpKeyVersion() {
    91  			ver, err := r.createNewKeyVersion(ctx, banner, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID))
    92  			if err != nil {
    93  				return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
    94  			}
    95  			err = r.rotateKey(ctx, secretClient, bannerEdgeID, banner, channel, ver)
    96  			if err != nil {
    97  				return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
    98  			}
    99  		}
   100  		if _, validKey := channel.LatestKeyVersion(); !validKey {
   101  			continue
   102  		}
   103  		for _, k := range channel.KeyVersions {
   104  			if k.IsExpired() {
   105  				key := r.kmsKey.KeyPath(banner.Name, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID), fmt.Sprintf("%d", k.Version))
   106  				req := &kmspb.UpdateCryptoKeyVersionRequest{
   107  					CryptoKeyVersion: &kmspb.CryptoKeyVersion{
   108  						Name:  key,
   109  						State: kmspb.CryptoKeyVersion_DISABLED,
   110  					},
   111  					UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}},
   112  				}
   113  				if _, err = r.KmsClient.UpdateCryptoKeyVersion(ctx, req); err != nil {
   114  					return recerr.New(fmt.Errorf("fail to disable key: %s, %w", key, err),
   115  						bannerAPI.PlatformSecretsCreationFailedReason)
   116  				}
   117  				r.Log.Info("encryption key disabled", "key", key)
   118  				_, err = r.ChannelService.DeleteChannelKeyVersion(ctx, k.ID)
   119  				if err != nil {
   120  					return recerr.New(fmt.Errorf("fail to delete channel key version: %s, %w", k.ID, err),
   121  						bannerAPI.PlatformSecretsCreationFailedReason)
   122  				}
   123  			}
   124  		}
   125  	}
   126  	return nil
   127  }
   128  
   129  func (r *EncryptionKeyManagementReconciler) createNewKeyVersion(ctx context.Context, banner *bannerAPI.Banner, channel string) (int, error) {
   130  	req := &kmspb.CreateCryptoKeyVersionRequest{Parent: r.kmsKey.Key(banner.Name, channel)}
   131  	result, err := r.KmsClient.CreateCryptoKeyVersion(ctx, req)
   132  	if err != nil {
   133  		return 0, fmt.Errorf("failed to create key version: %w", err)
   134  	}
   135  	// `projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*`.
   136  	token := strings.Split(result.Name, "/")
   137  	version := token[len(token)-1]
   138  
   139  	versionInt, err := strconv.Atoi(version)
   140  	if err != nil {
   141  		return 0, fmt.Errorf("failed to convert version to int: %w", err)
   142  	}
   143  	return versionInt, nil
   144  }
   145  
   146  func (r *EncryptionKeyManagementReconciler) rotateKey(ctx context.Context, secretClient secretManagerClient, bannerEdgeID uuid.UUID, banner *bannerAPI.Banner, channel channels.BannerChannel, version int) error {
   147  	ckv := channels.ChannelKeyVersion{
   148  		ChannelID:         channel.ID,
   149  		BannerEdgeID:      bannerEdgeID,
   150  		Version:           version,
   151  		SecretManagerLink: secretManagerLink(banner.Spec.GCP.ProjectID, fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID)),
   152  	}
   153  	_, err := r.ChannelService.CreateChannelKeyVersion(ctx, ckv)
   154  	if err != nil {
   155  		return fmt.Errorf("failed to create channel key version: %w", err)
   156  	}
   157  
   158  	key := r.kmsKey.KeyPath(banner.Name, fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID), fmt.Sprintf("%d", version))
   159  
   160  	pubKey, err := getPublicKeyWithRetry(ctx, r.KmsClient, key, r.ResourceTimeout)
   161  	if err != nil {
   162  		return fmt.Errorf("failed to get public key from kms to rotate: %w", err)
   163  	}
   164  
   165  	pk, err := edgeencrypt.NewPublicKey(pubKey.Pem, fmt.Sprintf("%d", version))
   166  	if err != nil {
   167  		return fmt.Errorf("failed to create public key: %w", err)
   168  	}
   169  	data, err := json.Marshal(pk)
   170  	if err != nil {
   171  		return fmt.Errorf("failed to marshal public key: %w", err)
   172  	}
   173  
   174  	err = saveToSecretManager(ctx, secretClient, fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID), data, version, map[string]string{"channel": channel.Name})
   175  	if err != nil {
   176  		return fmt.Errorf("failed to save rotated public key to secret manager: %w", err)
   177  	}
   178  	return nil
   179  }
   180  

View as plain text