From 41be794d067bbadf74ec48735920efa60fd001b4 Mon Sep 17 00:00:00 2001 From: Chaitanya Kandagatla Date: Thu, 16 Jan 2025 10:24:47 -0600 Subject: [PATCH] Add GCP KMS custom encryption support Signed-off-by: Chaitanya Kandagatla --- cmd/ocm/create/cluster/cmd.go | 185 +++++++++++++++++++++++++++++++++- pkg/cluster/cluster.go | 24 +++++ pkg/provider/encryption.go | 121 ++++++++++++++++++++++ 3 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 pkg/provider/encryption.go diff --git a/cmd/ocm/create/cluster/cmd.go b/cmd/ocm/create/cluster/cmd.go index eb0ef85d..2c47f927 100644 --- a/cmd/ocm/create/cluster/cmd.go +++ b/cmd/ocm/create/cluster/cmd.go @@ -58,6 +58,11 @@ const ( controlPlaneSubnetFlag = "control-plane-subnet" computePlaneSubnetFlag = "compute-subnet" pscSubnetFlag = "psc-subnet" + //Gcp Custom Encryption + KmsKeyLocationFlag = "kms-key-location" + kmsKeyRingFlag = "kms-key-ring" + kmsKeyNameFlag = "kms-key-name" + kmsKeySvcAccountFlag = "kms-key-service-account" ) var args struct { @@ -85,6 +90,7 @@ var args struct { gcpSecureBoot c.GcpSecurity gcpAuthentication c.GcpAuthentication gcpPrivateSvcConnect c.GcpPrivateSvcConnect + gcpEncryption c.GcpEncryption gcpWifConfig string etcdEncryption bool subscriptionType string @@ -399,6 +405,50 @@ func init() { ) arguments.SetQuestion(fs, "wif-config", "WIF configuration:") Cmd.RegisterFlagCompletionFunc("wif-config", arguments.MakeCompleteFunc(getWifConfigNameOptions)) + + addGcpEncryptionFlags(fs, &args.gcpEncryption) + +} + +func addGcpEncryptionFlags(fs *pflag.FlagSet, encryptionArgs *c.GcpEncryption) { + + fs.StringVar( + &encryptionArgs.KmsKeyLocation, + KmsKeyLocationFlag, + "", + "The location of KMS keyring in GCP for custom encryption."+ + "This should match the data center where the cluster's compute pool will be located.", + ) + arguments.SetQuestion(fs, KmsKeyLocationFlag, "KMS key location:") + Cmd.RegisterFlagCompletionFunc(kmsKeyRingFlag, arguments.MakeCompleteFunc(getKmsKeyLocationOptions)) + + fs.StringVar( + &encryptionArgs.KmsKeyRing, + kmsKeyRingFlag, + "", + "The name of the KMS key ring in GCP to use for custom encryption."+ + "The key ring should belong to the KMS location specified.", + ) + arguments.SetQuestion(fs, kmsKeyRingFlag, "KMS key ring:") + Cmd.RegisterFlagCompletionFunc(kmsKeyRingFlag, arguments.MakeCompleteFunc(getKmsKeyRingOptions)) + + fs.StringVar( + &encryptionArgs.KmsKeyName, + kmsKeyNameFlag, + "", + "The name of the KMS key in GCP to use for custom encryption."+ + "The key should belong to the KMS key ring specified.", + ) + arguments.SetQuestion(fs, kmsKeyNameFlag, "KMS key name:") + Cmd.RegisterFlagCompletionFunc(kmsKeyRingFlag, arguments.MakeCompleteFunc(getKmsKeyOptions)) + + fs.StringVar( + &encryptionArgs.KmsKeySvcAccount, + kmsKeySvcAccountFlag, + "", + "The name of the service account in GCP with access to KMS keyring and key specified for custom encryption", + ) + arguments.SetQuestion(fs, kmsKeySvcAccountFlag, "KMS key service-account name:") } func osdProviderOptions(_ *sdk.Connection) ([]arguments.Option, error) { @@ -560,6 +610,62 @@ func getWifConfigNameOptions(connection *sdk.Connection) ([]arguments.Option, er return provider.GetWifConfigNameOptions(connection.ClustersMgmt().V1()) } +func getKmsKeyLocationOptions(connection *sdk.Connection) ([]arguments.Option, error) { + + keyLocations, err := provider.GetGcpKmsKeyLocations(connection.ClustersMgmt().V1(), + args.ccs, args.gcpAuthentication, args.region) + if err != nil { + return nil, err + } + + options := []arguments.Option{} + for _, keyLoc := range keyLocations { + options = append(options, arguments.Option{ + Value: keyLoc, + Description: keyLoc, + }) + } + return options, nil +} + +func getKmsKeyRingOptions(connection *sdk.Connection) ([]arguments.Option, error) { + + keyRingList, err := provider.GetGcpKmsKeyRings(connection.ClustersMgmt().V1(), + args.ccs, args.gcpAuthentication, args.gcpEncryption.KmsKeyLocation) + if err != nil { + return nil, err + } + + options := []arguments.Option{} + for _, kr := range keyRingList { + options = append(options, arguments.Option{ + Value: kr.Name(), + Description: kr.Name(), + }) + } + + return options, nil +} + +func getKmsKeyOptions(connection *sdk.Connection) ([]arguments.Option, error) { + + keysList, err := provider.GetGcpKmsKeys(connection.ClustersMgmt().V1(), + args.ccs, args.gcpAuthentication, args.gcpEncryption.KmsKeyLocation, args.gcpEncryption.KmsKeyRing) + if err != nil { + return nil, err + } + + options := []arguments.Option{} + for _, key := range keysList { + options = append(options, arguments.Option{ + Value: key.Name(), + Description: key.Name(), + }) + } + + return options, nil +} + func networkTypeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{c.NetworkTypeSDN, c.NetworkTypeOVN}, cobra.ShellCompDirectiveDefault } @@ -661,21 +767,26 @@ func preRun(cmd *cobra.Command, argv []string) error { return err } - err = arguments.PromptBool(fs, "multi-az") + regions, err := getRegionOptions(connection) + if err != nil { + return err + } + err = arguments.PromptOrCheckOneOf(fs, "region", regions) if err != nil { return err } - err = promptSecureBoot(fs) + err = arguments.PromptBool(fs, "multi-az") if err != nil { return err } - regions, err := getRegionOptions(connection) + err = promptSecureBoot(fs) if err != nil { return err } - err = arguments.PromptOrCheckOneOf(fs, "region", regions) + + err = promptGcpCustomEncryption(fs, connection) if err != nil { return err } @@ -839,6 +950,7 @@ func run(cmd *cobra.Command, argv []string) error { GcpSecurity: args.gcpSecureBoot, GcpAuthentication: args.gcpAuthentication, GcpPrivateSvcConnect: args.gcpPrivateSvcConnect, + GcpEncryption: args.gcpEncryption, } cluster, err := c.CreateCluster(connection.ClustersMgmt().V1(), clusterConfig, args.dryRun) @@ -1550,6 +1662,71 @@ func promptSecureBoot(fs *pflag.FlagSet) error { return nil } +func promptGcpCustomEncryption(fs *pflag.FlagSet, connection *sdk.Connection) error { + + if !args.ccs.Enabled || args.provider != c.ProviderGCP { + return nil + } + + isCustomKeys := args.gcpEncryption.KmsKeyLocation != "" || + args.gcpEncryption.KmsKeyRing != "" || + args.gcpEncryption.KmsKeyName != "" || + args.gcpEncryption.KmsKeySvcAccount != "" + + if !isCustomKeys && args.interactive { + var err error + isCustomKeys, err = interactive.GetBool(interactive.Input{ + Question: "Use Custom KMS Keys", + Help: "To use custom encryption keys managed via GCP Key management service.", + Default: false, + }) + if err != nil { + return err + } + } + + if !isCustomKeys { + return nil + } + + KmsKeyLocationOptions, err := getKmsKeyLocationOptions(connection) + if err != nil { + return err + } + + err = arguments.PromptOrCheckOneOf(fs, KmsKeyLocationFlag, KmsKeyLocationOptions) + if err != nil { + return err + } + + keyringOptions, err := getKmsKeyRingOptions(connection) + if err != nil { + return err + } + + err = arguments.PromptOrCheckOneOf(fs, kmsKeyRingFlag, keyringOptions) + if err != nil { + return err + } + + keysOptions, err := getKmsKeyOptions(connection) + if err != nil { + return err + } + + err = arguments.PromptOrCheckOneOf(fs, kmsKeyNameFlag, keysOptions) + if err != nil { + return err + } + + err = arguments.PromptString(fs, kmsKeySvcAccountFlag) + if err != nil { + return err + } + + return nil +} + func promptPrivateServiceConnect(fs *pflag.FlagSet) error { if args.provider != c.ProviderGCP || !args.existingVPC.Enabled || !args.private { diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index e60a3bd5..4486bf81 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -102,6 +102,9 @@ type Spec struct { // GCP PrivateServiceConnect settings GcpPrivateSvcConnect GcpPrivateSvcConnect + + //Includes Custom KMS encryption key settings + GcpEncryption GcpEncryption } type Autoscaling struct { @@ -170,6 +173,13 @@ type GcpPrivateSvcConnect struct { SvcAttachmentSubnet string } +type GcpEncryption struct { + KmsKeySvcAccount string + KmsKeyLocation string + KmsKeyRing string + KmsKeyName string +} + type AddOnItem struct { ID string Name string @@ -489,6 +499,15 @@ func CreateCluster(cmv1Client *cmv1.Client, config Spec, dryRun bool) (*cmv1.Clu gcpBuilder.Security(gcpSecurity) } + if useGcpCustomEncryption(config.GcpEncryption) { + gcpEncryption := cmv1.NewGCPEncryptionKey(). + KeyLocation(config.GcpEncryption.KmsKeyLocation). + KeyRing(config.GcpEncryption.KmsKeyRing). + KeyName(config.GcpEncryption.KmsKeyName). + KMSKeyServiceAccount(config.GcpEncryption.KmsKeySvcAccount) + clusterBuilder.GCPEncryptionKey(gcpEncryption) + } + if isGcpPsc(config.GcpPrivateSvcConnect) { gcpPsc := cmv1.NewGcpPrivateServiceConnect().ServiceAttachmentSubnet(config.GcpPrivateSvcConnect.SvcAttachmentSubnet) gcpBuilder.PrivateServiceConnect(gcpPsc) @@ -563,6 +582,11 @@ func isGCPNetworkExists(existingVPC ExistingVPC) bool { existingVPC.ComputeSubnet != "" || existingVPC.VPCProjectID != "" } +func useGcpCustomEncryption(gcpEncData GcpEncryption) bool { + return gcpEncData.KmsKeyLocation != "" || gcpEncData.KmsKeyRing != "" || + gcpEncData.KmsKeyName != "" || gcpEncData.KmsKeySvcAccount != "" +} + func isGCPSharedVPC(existingVPC ExistingVPC) bool { return existingVPC.VPCProjectID != "" } diff --git a/pkg/provider/encryption.go b/pkg/provider/encryption.go new file mode 100644 index 00000000..96589270 --- /dev/null +++ b/pkg/provider/encryption.go @@ -0,0 +1,121 @@ +package provider + +import ( + "fmt" + "strings" + + "github.com/openshift-online/ocm-cli/pkg/cluster" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/pkg/errors" +) + +func GetGcpKmsKeyLocations(client *cmv1.Client, ccs cluster.CCS, + gcpAuth cluster.GcpAuthentication, region string) ([]string, error) { + + gcpDataBuilder, err := getGcpCloudProviderDataBuilder(gcpAuth, ccs) + if err != nil { + return nil, err + } + + cloudProviderData, err := gcpDataBuilder.Build() + if err != nil { + return nil, errors.Wrapf(err, "failed to build GCP provider data") + } + + response, err := client.GCPInquiries().Regions().Search(). + Page(1).Size(-1).Body(cloudProviderData).Send() + + if err != nil { + return nil, err + } + + var keyLocations []string + for _, cloudRegion := range response.Items().Slice() { + keyLocationsStr, ok := cloudRegion.GetKMSLocationID() + if !ok { + return nil, errors.Wrapf(err, "failed to build GCP provider data") + } + keyLocations = strings.Split(keyLocationsStr, ",") + } + + return keyLocations, err +} + +func GetGcpKmsKeyRings(client *cmv1.Client, ccs cluster.CCS, + gcpAuth cluster.GcpAuthentication, keyLocation string) ([]*cmv1.KeyRing, error) { + + gcpDataBuilder, err := getGcpCloudProviderDataBuilder(gcpAuth, ccs) + if err != nil { + return nil, err + } + + cloudProviderData, err := gcpDataBuilder. + KeyLocation(keyLocation). + Build() + if err != nil { + return nil, errors.Wrapf(err, "failed to build GCP provider data") + } + response, err := client.GCPInquiries().KeyRings().Search(). + Page(1).Size(-1).Body(cloudProviderData).Send() + + if err != nil { + return nil, err + } + + return response.Items().Slice(), err +} + +func GetGcpKmsKeys(client *cmv1.Client, ccs cluster.CCS, + gcpAuth cluster.GcpAuthentication, keyLocation string, keyRing string) ([]*cmv1.EncryptionKey, error) { + + gcpDataBuilder, err := getGcpCloudProviderDataBuilder(gcpAuth, ccs) + if err != nil { + return nil, err + } + + cloudProviderData, err := gcpDataBuilder. + KeyLocation(keyLocation). + KeyRingName(keyRing). + Build() + if err != nil { + return nil, errors.Wrapf(err, "failed to build GCP provider data") + } + + response, err := client.GCPInquiries().EncryptionKeys().Search(). + Page(1).Size(-1).Body(cloudProviderData).Send() + + if err != nil { + return nil, err + } + + return response.Items().Slice(), err +} + +func getGcpCloudProviderDataBuilder(gcpAuth cluster.GcpAuthentication, + ccs cluster.CCS) (*cmv1.CloudProviderDataBuilder, error) { + + gcpBuilder := cmv1.NewGCP() + + switch gcpAuth.Type { + case cluster.AuthenticationWif: + gcpAuth := cmv1.NewGcpAuthentication(). + Kind(cmv1.WifConfigKind). + Id(gcpAuth.Id) + gcpBuilder.Authentication(gcpAuth) + case cluster.AuthenticationKey: + gcpBuilder.ProjectID(ccs.GCP.ProjectID). + ClientEmail(ccs.GCP.ClientEmail). + Type(ccs.GCP.Type). + PrivateKey(ccs.GCP.PrivateKey). + PrivateKeyID(ccs.GCP.PrivateKeyID). + AuthProviderX509CertURL(ccs.GCP.AuthProviderX509CertURL). + AuthURI(ccs.GCP.AuthURI).TokenURI(ccs.GCP.TokenURI). + ClientX509CertURL(ccs.GCP.ClientX509CertURL). + ClientID(ccs.GCP.ClientID).TokenURI(ccs.GCP.TokenURI) + default: + return nil, errors.New( + fmt.Sprintf("failed to build GCP provider data, unexpected GCP authentication method %q", gcpAuth.Type)) + } + + return cmv1.NewCloudProviderData().GCP(gcpBuilder), nil +}