diff --git a/api/v1beta1/gcpmachine_types.go b/api/v1beta1/gcpmachine_types.go index 846385558..52faeaaeb 100644 --- a/api/v1beta1/gcpmachine_types.go +++ b/api/v1beta1/gcpmachine_types.go @@ -54,6 +54,9 @@ type AttachedDiskSpec struct { // Defaults to 30GB. For "local-ssd" size is always 375GB. // +optional Size *int64 `json:"size,omitempty"` + // EncryptionKey defines the KMS key to be used to encrypt the disk. + // +optional + EncryptionKey *CustomerEncryptionKey `json:"encryptionKey,omitempty"` } // IPForwarding represents the IP forwarding configuration for the GCP machine. @@ -146,6 +149,72 @@ const ( HostMaintenancePolicyTerminate HostMaintenancePolicy = "Terminate" ) +// KeyType is a type for disk encryption. +type KeyType string + +const ( + // CustomerManagedKey (CMEK) references an encryption key stored in Google Cloud KMS. + CustomerManagedKey KeyType = "Managed" + // CustomerSuppliedKey (CSEK) specifies an encryption key to use. + CustomerSuppliedKey KeyType = "Supplied" +) + +// ManagedKey is a reference to a key managed by the Cloud Key Management Service. +type ManagedKey struct { + // KMSKeyName is the name of the encryption key that is stored in Google Cloud KMS. For example: + // "kmsKeyName": "projects/kms_project_id/locations/region/keyRings/key_region/cryptoKeys/key + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`projects\/[-_[A-Za-z0-9]+\/locations\/[-_[A-Za-z0-9]+\/keyRings\/[-_[A-Za-z0-9]+\/cryptoKeys\/[-_[A-Za-z0-9]+` + // +kubebuilder:validation:MaxLength=160 + KMSKeyName string `json:"kmsKeyName,omitempty"` +} + +// SuppliedKey contains a key for disk encryption. Either RawKey or RSAEncryptedKey must be provided. +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:validation:MaxProperties=1 +type SuppliedKey struct { + // RawKey specifies a 256-bit customer-supplied encryption key, encoded in RFC 4648 + // base64 to either encrypt or decrypt this resource. You can provide either the rawKey or the rsaEncryptedKey. + // For example: "rawKey": "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0=" + // +optional + RawKey []byte `json:"rawKey,omitempty"` + // RSAEncryptedKey specifies an RFC 4648 base64 encoded, RSA-wrapped 2048-bit customer-supplied encryption + // key to either encrypt or decrypt this resource. You can provide either the rawKey or the + // rsaEncryptedKey. + // For example: "rsaEncryptedKey": "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHi + // z0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDi + // D6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==" + // The key must meet the following requirements before you can provide it to Compute Engine: + // 1. The key is wrapped using a RSA public key certificate provided by Google. + // 2. After being wrapped, the key must be encoded in RFC 4648 base64 encoding. + // Gets the RSA public key certificate provided by Google at: https://cloud-certs.storage.googleapis.com/google-cloud-csek-ingress.pem + // +optional + RSAEncryptedKey []byte `json:"rsaEncryptedKey,omitempty"` +} + +// CustomerEncryptionKey supports both Customer-Managed or Customer-Supplied encryption keys . +type CustomerEncryptionKey struct { + // KeyType is the type of encryption key. Must be either Managed, aka Customer-Managed Encryption Key (CMEK) or + // Supplied, aka Customer-Supplied EncryptionKey (CSEK). + // +kubebuilder:validation:Enum=Managed;Supplied + KeyType KeyType `json:"keyType"` + // KMSKeyServiceAccount is the service account being used for the encryption request for the given KMS key. + // If absent, the Compute Engine default service account is used. For example: + // "kmsKeyServiceAccount": "name@project_id.iam.gserviceaccount.com. + // The maximum length is based on the Service Account ID (max 30), Project (max 30), and a valid gcloud email + // suffix ("iam.gserviceaccount.com"). + // +kubebuilder:validation:MaxLength=85 + // +kubebuilder:validation:Pattern=`[-_[A-Za-z0-9]+@[-_[A-Za-z0-9]+.iam.gserviceaccount.com` + // +optional + KMSKeyServiceAccount *string `json:"kmsKeyServiceAccount,omitempty"` + // ManagedKey references keys managed by the Cloud Key Management Service. This should be set when KeyType is Managed. + // +optional + ManagedKey *ManagedKey `json:"managedKey,omitempty"` + // SuppliedKey provides the key used to create or manage a disk. This should be set when KeyType is Managed. + // +optional + SuppliedKey *SuppliedKey `json:"suppliedKey,omitempty"` +} + // GCPMachineSpec defines the desired state of GCPMachine. type GCPMachineSpec struct { // InstanceType is the type of instance to create. Example: n1.standard-2 @@ -252,6 +321,10 @@ type GCPMachineSpec struct { // +kubebuilder:validation:Enum=Enabled;Disabled // +optional ConfidentialCompute *ConfidentialComputePolicy `json:"confidentialCompute,omitempty"` + + // RootDiskEncryptionKey defines the KMS key to be used to encrypt the root disk. + // +optional + RootDiskEncryptionKey *CustomerEncryptionKey `json:"rootDiskEncryptionKey,omitempty"` } // MetadataItem defines a single piece of metadata associated with an instance. diff --git a/api/v1beta1/gcpmachine_webhook.go b/api/v1beta1/gcpmachine_webhook.go index bc6dc9488..2b8057ad4 100644 --- a/api/v1beta1/gcpmachine_webhook.go +++ b/api/v1beta1/gcpmachine_webhook.go @@ -50,7 +50,11 @@ var _ webhook.Validator = &GCPMachine{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. func (m *GCPMachine) ValidateCreate() (admission.Warnings, error) { clusterlog.Info("validate create", "name", m.Name) - return nil, validateConfidentialCompute(m.Spec) + + if err := validateConfidentialCompute(m.Spec); err != nil { + return nil, err + } + return nil, validateCustomerEncryptionKey(m.Spec) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. @@ -117,3 +121,39 @@ func validateConfidentialCompute(spec GCPMachineSpec) error { } return nil } + +func checkKeyType(key *CustomerEncryptionKey) error { + switch key.KeyType { + case CustomerManagedKey: + if key.ManagedKey == nil || key.SuppliedKey != nil { + return fmt.Errorf("CustomerEncryptionKey KeyType of Managed requires only ManagedKey to be set") + } + case CustomerSuppliedKey: + if key.SuppliedKey == nil || key.ManagedKey != nil { + return fmt.Errorf("CustomerEncryptionKey KeyType of Supplied requires only SuppliedKey to be set") + } + if len(key.SuppliedKey.RawKey) > 0 && len(key.SuppliedKey.RSAEncryptedKey) > 0 { + return fmt.Errorf("CustomerEncryptionKey KeyType of Supplied requires either RawKey or RSAEncryptedKey to be set, not both") + } + default: + return fmt.Errorf("invalid value for CustomerEncryptionKey KeyType %s", key.KeyType) + } + return nil +} + +func validateCustomerEncryptionKey(spec GCPMachineSpec) error { + if spec.RootDiskEncryptionKey != nil { + if err := checkKeyType(spec.RootDiskEncryptionKey); err != nil { + return err + } + } + + for _, disk := range spec.AdditionalDisks { + if disk.EncryptionKey != nil { + if err := checkKeyType(disk.EncryptionKey); err != nil { + return err + } + } + } + return nil +} diff --git a/api/v1beta1/gcpmachine_webhook_test.go b/api/v1beta1/gcpmachine_webhook_test.go index 2eab4f9bf..a1b38a38c 100644 --- a/api/v1beta1/gcpmachine_webhook_test.go +++ b/api/v1beta1/gcpmachine_webhook_test.go @@ -85,6 +85,86 @@ func TestGCPMachine_ValidateCreate(t *testing.T) { }, wantErr: true, }, + { + name: "GCPMachine with RootDiskEncryptionKey KeyType Managed and Managed field set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + RootDiskEncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerManagedKey, + ManagedKey: &ManagedKey{ + KMSKeyName: "projects/my-project/locations/us-central1/keyRings/us-central1/cryptoKeys/some-key", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "GCPMachine with RootDiskEncryptionKey KeyType Managed and Managed field not set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + RootDiskEncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerManagedKey, + }, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with RootDiskEncryptionKey KeyType Supplied and Supplied field not set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + RootDiskEncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerSuppliedKey, + }, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with AdditionalDisk Encryption KeyType Managed and Managed field not set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + AdditionalDisks: []AttachedDiskSpec{ + { + EncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerManagedKey, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with RootDiskEncryptionKey KeyType Supplied and one Supplied field set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + RootDiskEncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerSuppliedKey, + SuppliedKey: &SuppliedKey{ + RawKey: []byte("SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "GCPMachine with RootDiskEncryptionKey KeyType Supplied and both Supplied fields set", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + RootDiskEncryptionKey: &CustomerEncryptionKey{ + KeyType: CustomerSuppliedKey, + SuppliedKey: &SuppliedKey{ + RawKey: []byte("SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="), + RSAEncryptedKey: []byte("SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="), + }, + }, + }, + }, + wantErr: true, + }, } for _, test := range tests { test := test diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 5ba5b3065..d5a60f830 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -40,6 +40,11 @@ func (in *AttachedDiskSpec) DeepCopyInto(out *AttachedDiskSpec) { *out = new(int64) **out = **in } + if in.EncryptionKey != nil { + in, out := &in.EncryptionKey, &out.EncryptionKey + *out = new(CustomerEncryptionKey) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AttachedDiskSpec. @@ -79,6 +84,36 @@ func (in *BuildParams) DeepCopy() *BuildParams { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomerEncryptionKey) DeepCopyInto(out *CustomerEncryptionKey) { + *out = *in + if in.KMSKeyServiceAccount != nil { + in, out := &in.KMSKeyServiceAccount, &out.KMSKeyServiceAccount + *out = new(string) + **out = **in + } + if in.ManagedKey != nil { + in, out := &in.ManagedKey, &out.ManagedKey + *out = new(ManagedKey) + **out = **in + } + if in.SuppliedKey != nil { + in, out := &in.SuppliedKey, &out.SuppliedKey + *out = new(SuppliedKey) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomerEncryptionKey. +func (in *CustomerEncryptionKey) DeepCopy() *CustomerEncryptionKey { + if in == nil { + return nil + } + out := new(CustomerEncryptionKey) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Filter) DeepCopyInto(out *Filter) { *out = *in @@ -459,6 +494,11 @@ func (in *GCPMachineSpec) DeepCopyInto(out *GCPMachineSpec) { *out = new(ConfidentialComputePolicy) **out = **in } + if in.RootDiskEncryptionKey != nil { + in, out := &in.RootDiskEncryptionKey, &out.RootDiskEncryptionKey + *out = new(CustomerEncryptionKey) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPMachineSpec. @@ -633,6 +673,21 @@ func (in Labels) DeepCopy() Labels { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedKey) DeepCopyInto(out *ManagedKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKey. +func (in *ManagedKey) DeepCopy() *ManagedKey { + if in == nil { + return nil + } + out := new(ManagedKey) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MetadataItem) DeepCopyInto(out *MetadataItem) { *out = *in @@ -906,3 +961,28 @@ func (in Subnets) DeepCopy() Subnets { in.DeepCopyInto(out) return *out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SuppliedKey) DeepCopyInto(out *SuppliedKey) { + *out = *in + if in.RawKey != nil { + in, out := &in.RawKey, &out.RawKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.RSAEncryptedKey != nil { + in, out := &in.RSAEncryptedKey, &out.RSAEncryptedKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SuppliedKey. +func (in *SuppliedKey) DeepCopy() *SuppliedKey { + if in == nil { + return nil + } + out := new(SuppliedKey) + in.DeepCopyInto(out) + return out +} diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 3a0f9f092..15b0dc141 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -235,7 +235,7 @@ func (m *MachineScope) InstanceImageSpec() *compute.AttachedDisk { diskType = *t } - return &compute.AttachedDisk{ + disk := &compute.AttachedDisk{ AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ @@ -245,6 +245,27 @@ func (m *MachineScope) InstanceImageSpec() *compute.AttachedDisk { SourceImage: sourceImage, }, } + + if m.GCPMachine.Spec.RootDiskEncryptionKey != nil { + if m.GCPMachine.Spec.RootDiskEncryptionKey.KeyType == infrav1.CustomerManagedKey && m.GCPMachine.Spec.RootDiskEncryptionKey.ManagedKey != nil { + disk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + KmsKeyName: m.GCPMachine.Spec.RootDiskEncryptionKey.ManagedKey.KMSKeyName, + } + if m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount != nil { + disk.DiskEncryptionKey.KmsKeyServiceAccount = *m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount + } + } else if m.GCPMachine.Spec.RootDiskEncryptionKey.KeyType == infrav1.CustomerSuppliedKey && m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey != nil { + disk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + RawKey: string(m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey.RawKey), + RsaEncryptedKey: string(m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey.RSAEncryptedKey), + } + if m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount != nil { + disk.DiskEncryptionKey.KmsKeyServiceAccount = *m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount + } + } + } + + return disk } // InstanceAdditionalDiskSpec returns compute instance additional attched-disk spec. @@ -269,6 +290,25 @@ func (m *MachineScope) InstanceAdditionalDiskSpec() []*compute.AttachedDisk { // https://cloud.google.com/compute/docs/disks/local-ssd#choose_an_interface additionalDisk.Interface = "NVME" } + if disk.EncryptionKey != nil { + if m.GCPMachine.Spec.RootDiskEncryptionKey.KeyType == infrav1.CustomerManagedKey && m.GCPMachine.Spec.RootDiskEncryptionKey.ManagedKey != nil { + additionalDisk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + KmsKeyName: m.GCPMachine.Spec.RootDiskEncryptionKey.ManagedKey.KMSKeyName, + } + if m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount != nil { + additionalDisk.DiskEncryptionKey.KmsKeyServiceAccount = *m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount + } + } else if m.GCPMachine.Spec.RootDiskEncryptionKey.KeyType == infrav1.CustomerSuppliedKey && m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey != nil { + additionalDisk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + RawKey: string(m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey.RawKey), + RsaEncryptedKey: string(m.GCPMachine.Spec.RootDiskEncryptionKey.SuppliedKey.RSAEncryptedKey), + } + if m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount != nil { + additionalDisk.DiskEncryptionKey.KmsKeyServiceAccount = *m.GCPMachine.Spec.RootDiskEncryptionKey.KMSKeyServiceAccount + } + } + } + additionalDisks = append(additionalDisks, additionalDisk) } diff --git a/cloud/services/compute/instances/reconcile_test.go b/cloud/services/compute/instances/reconcile_test.go index 54019c3f8..dd6b6757c 100644 --- a/cloud/services/compute/instances/reconcile_test.go +++ b/cloud/services/compute/instances/reconcile_test.go @@ -460,12 +460,12 @@ func TestService_createOrGetInstance(t *testing.T) { ResourceManagerTags: map[string]string{}, }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", - Scheduling: &compute.Scheduling{ - OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyTerminate)), - }, ConfidentialInstanceConfig: &compute.ConfidentialInstanceConfig{ EnableConfidentialCompute: true, }, + Scheduling: &compute.Scheduling{ + OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyTerminate)), + }, ServiceAccounts: []*compute.ServiceAccount{ { Email: "default", @@ -609,6 +609,225 @@ func TestService_createOrGetInstance(t *testing.T) { Zone: "us-central1-a", }, }, + { + name: "instance does not exist (should create instance) with Customer-Managed boot DiskEncryption", + scope: func() Scope { + machineScope.GCPMachine = getFakeGCPMachine() + diskEncryption := infrav1.CustomerEncryptionKey{ + KeyType: infrav1.CustomerManagedKey, + ManagedKey: &infrav1.ManagedKey{ + KMSKeyName: "projects/my-project/locations/us-central1/keyRings/us-central1/cryptoKeys/some-key", + }, + } + machineScope.GCPMachine.Spec.RootDiskEncryptionKey = &diskEncryption + return machineScope + }, + mockInstance: &cloud.MockInstances{ + ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"}, + Objects: map[meta.Key]*cloud.MockInstancesObj{}, + }, + want: &compute.Instance{ + Name: "my-machine", + CanIpForward: true, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + InitializeParams: &compute.AttachedDiskInitializeParams{ + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, + }, + DiskEncryptionKey: &compute.CustomerEncryptionKey{ + KmsKeyName: "projects/my-project/locations/us-central1/keyRings/us-central1/cryptoKeys/some-key", + }, + }, + }, + Labels: map[string]string{ + "capg-role": "node", + "capg-cluster-my-cluster": "owned", + "foo": "bar", + }, + MachineType: "zones/us-central1-c/machineTypes", + Metadata: &compute.Metadata{ + Items: []*compute.MetadataItems{ + { + Key: "user-data", + Value: ptr.To[string]("Zm9vCg=="), + }, + }, + }, + NetworkInterfaces: []*compute.NetworkInterface{ + { + Network: "projects/my-proj/global/networks/default", + }, + }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, + SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", + Scheduling: &compute.Scheduling{}, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "default", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + }, + }, + Tags: &compute.Tags{ + Items: []string{ + "my-cluster-node", + "my-cluster", + }, + }, + Zone: "us-central1-c", + }, + }, + { + name: "instance does not exist (should create instance) with Customer-Supplied Raw DiskEncryption", + scope: func() Scope { + machineScope.GCPMachine = getFakeGCPMachine() + diskEncryption := infrav1.CustomerEncryptionKey{ + KeyType: infrav1.CustomerSuppliedKey, + SuppliedKey: &infrav1.SuppliedKey{ + RawKey: []byte("SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="), + }, + } + machineScope.GCPMachine.Spec.RootDiskEncryptionKey = &diskEncryption + return machineScope + }, + mockInstance: &cloud.MockInstances{ + ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"}, + Objects: map[meta.Key]*cloud.MockInstancesObj{}, + }, + want: &compute.Instance{ + Name: "my-machine", + CanIpForward: true, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + InitializeParams: &compute.AttachedDiskInitializeParams{ + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, + }, + DiskEncryptionKey: &compute.CustomerEncryptionKey{ + RawKey: "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0=", + }, + }, + }, + Labels: map[string]string{ + "capg-role": "node", + "capg-cluster-my-cluster": "owned", + "foo": "bar", + }, + MachineType: "zones/us-central1-c/machineTypes", + Metadata: &compute.Metadata{ + Items: []*compute.MetadataItems{ + { + Key: "user-data", + Value: ptr.To[string]("Zm9vCg=="), + }, + }, + }, + NetworkInterfaces: []*compute.NetworkInterface{ + { + Network: "projects/my-proj/global/networks/default", + }, + }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, + SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", + Scheduling: &compute.Scheduling{}, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "default", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + }, + }, + Tags: &compute.Tags{ + Items: []string{ + "my-cluster-node", + "my-cluster", + }, + }, + Zone: "us-central1-c", + }, + }, + { + name: "instance does not exist (should create instance) with Customer-Supplied RSA DiskEncryption", + scope: func() Scope { + machineScope.GCPMachine = getFakeGCPMachine() + diskEncryption := infrav1.CustomerEncryptionKey{ + KeyType: infrav1.CustomerSuppliedKey, + SuppliedKey: &infrav1.SuppliedKey{ + RSAEncryptedKey: []byte("ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHiz0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDiD6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe=="), + }, + } + machineScope.GCPMachine.Spec.RootDiskEncryptionKey = &diskEncryption + return machineScope + }, + mockInstance: &cloud.MockInstances{ + ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"}, + Objects: map[meta.Key]*cloud.MockInstancesObj{}, + }, + want: &compute.Instance{ + Name: "my-machine", + CanIpForward: true, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + InitializeParams: &compute.AttachedDiskInitializeParams{ + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, + }, + DiskEncryptionKey: &compute.CustomerEncryptionKey{ + RsaEncryptedKey: "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHiz0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDiD6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==", + }, + }, + }, + Labels: map[string]string{ + "capg-role": "node", + "capg-cluster-my-cluster": "owned", + "foo": "bar", + }, + MachineType: "zones/us-central1-c/machineTypes", + Metadata: &compute.Metadata{ + Items: []*compute.MetadataItems{ + { + Key: "user-data", + Value: ptr.To[string]("Zm9vCg=="), + }, + }, + }, + NetworkInterfaces: []*compute.NetworkInterface{ + { + Network: "projects/my-proj/global/networks/default", + }, + }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, + SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", + Scheduling: &compute.Scheduling{}, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "default", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + }, + }, + Tags: &compute.Tags{ + Items: []string{ + "my-cluster-node", + "my-cluster", + }, + }, + Zone: "us-central1-c", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml index c965b68be..61311c09e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml @@ -69,6 +69,77 @@ spec: disk 3. "local-ssd" - Local SSD disk (https://cloud.google.com/compute/docs/disks/local-ssd). Default is "pd-standard".' type: string + encryptionKey: + description: EncryptionKey defines the KMS key to be used to + encrypt the disk. + properties: + keyType: + description: KeyType is the type of encryption key. Must + be either Managed, aka Customer-Managed Encryption Key + (CMEK) or Supplied, aka Customer-Supplied EncryptionKey + (CSEK). + enum: + - Managed + - Supplied + type: string + kmsKeyServiceAccount: + description: 'KMSKeyServiceAccount is the service account + being used for the encryption request for the given KMS + key. If absent, the Compute Engine default service account + is used. For example: "kmsKeyServiceAccount": "name@project_id.iam.gserviceaccount.com. + The maximum length is based on the Service Account ID + (max 30), Project (max 30), and a valid gcloud email suffix + ("iam.gserviceaccount.com").' + maxLength: 85 + pattern: '[-_[A-Za-z0-9]+@[-_[A-Za-z0-9]+.iam.gserviceaccount.com' + type: string + managedKey: + description: ManagedKey references keys managed by the Cloud + Key Management Service. This should be set when KeyType + is Managed. + properties: + kmsKeyName: + description: 'KMSKeyName is the name of the encryption + key that is stored in Google Cloud KMS. For example: + "kmsKeyName": "projects/kms_project_id/locations/region/keyRings/key_region/cryptoKeys/key' + maxLength: 160 + pattern: projects\/[-_[A-Za-z0-9]+\/locations\/[-_[A-Za-z0-9]+\/keyRings\/[-_[A-Za-z0-9]+\/cryptoKeys\/[-_[A-Za-z0-9]+ + type: string + type: object + suppliedKey: + description: SuppliedKey provides the key used to create + or manage a disk. This should be set when KeyType is Managed. + maxProperties: 1 + minProperties: 1 + properties: + rawKey: + description: 'RawKey specifies a 256-bit customer-supplied + encryption key, encoded in RFC 4648 base64 to either + encrypt or decrypt this resource. You can provide + either the rawKey or the rsaEncryptedKey. For example: + "rawKey": "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="' + format: byte + type: string + rsaEncryptedKey: + description: 'RSAEncryptedKey specifies an RFC 4648 + base64 encoded, RSA-wrapped 2048-bit customer-supplied + encryption key to either encrypt or decrypt this resource. + You can provide either the rawKey or the rsaEncryptedKey. + For example: "rsaEncryptedKey": "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHi + z0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDi + D6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==" + The key must meet the following requirements before + you can provide it to Compute Engine: 1. The key is + wrapped using a RSA public key certificate provided + by Google. 2. After being wrapped, the key must be + encoded in RFC 4648 base64 encoding. Gets the RSA + public key certificate provided by Google at: https://cloud-certs.storage.googleapis.com/google-cloud-csek-ingress.pem' + format: byte + type: string + type: object + required: + - keyType + type: object size: description: Size is the size of the disk in GBs. Defaults to 30GB. For "local-ssd" size is always 375GB. @@ -226,6 +297,73 @@ spec: types of root volumes: 1. "pd-standard" - Standard (HDD) persistent disk 2. "pd-ssd" - SSD persistent disk Default is "pd-standard".' type: string + rootDiskEncryptionKey: + description: RootDiskEncryptionKey defines the KMS key to be used + to encrypt the root disk. + properties: + keyType: + description: KeyType is the type of encryption key. Must be either + Managed, aka Customer-Managed Encryption Key (CMEK) or Supplied, + aka Customer-Supplied EncryptionKey (CSEK). + enum: + - Managed + - Supplied + type: string + kmsKeyServiceAccount: + description: 'KMSKeyServiceAccount is the service account being + used for the encryption request for the given KMS key. If absent, + the Compute Engine default service account is used. For example: + "kmsKeyServiceAccount": "name@project_id.iam.gserviceaccount.com. + The maximum length is based on the Service Account ID (max 30), + Project (max 30), and a valid gcloud email suffix ("iam.gserviceaccount.com").' + maxLength: 85 + pattern: '[-_[A-Za-z0-9]+@[-_[A-Za-z0-9]+.iam.gserviceaccount.com' + type: string + managedKey: + description: ManagedKey references keys managed by the Cloud Key + Management Service. This should be set when KeyType is Managed. + properties: + kmsKeyName: + description: 'KMSKeyName is the name of the encryption key + that is stored in Google Cloud KMS. For example: "kmsKeyName": + "projects/kms_project_id/locations/region/keyRings/key_region/cryptoKeys/key' + maxLength: 160 + pattern: projects\/[-_[A-Za-z0-9]+\/locations\/[-_[A-Za-z0-9]+\/keyRings\/[-_[A-Za-z0-9]+\/cryptoKeys\/[-_[A-Za-z0-9]+ + type: string + type: object + suppliedKey: + description: SuppliedKey provides the key used to create or manage + a disk. This should be set when KeyType is Managed. + maxProperties: 1 + minProperties: 1 + properties: + rawKey: + description: 'RawKey specifies a 256-bit customer-supplied + encryption key, encoded in RFC 4648 base64 to either encrypt + or decrypt this resource. You can provide either the rawKey + or the rsaEncryptedKey. For example: "rawKey": "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="' + format: byte + type: string + rsaEncryptedKey: + description: 'RSAEncryptedKey specifies an RFC 4648 base64 + encoded, RSA-wrapped 2048-bit customer-supplied encryption + key to either encrypt or decrypt this resource. You can + provide either the rawKey or the rsaEncryptedKey. For example: + "rsaEncryptedKey": "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHi + z0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDi + D6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==" + The key must meet the following requirements before you + can provide it to Compute Engine: 1. The key is wrapped + using a RSA public key certificate provided by Google. 2. + After being wrapped, the key must be encoded in RFC 4648 + base64 encoding. Gets the RSA public key certificate provided + by Google at: https://cloud-certs.storage.googleapis.com/google-cloud-csek-ingress.pem' + format: byte + type: string + type: object + required: + - keyType + type: object serviceAccounts: description: 'ServiceAccount specifies the service account email and which scopes to assign to the machine. Defaults to: email: "default", diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml index ceacc3c0f..ead89e737 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml @@ -80,6 +80,81 @@ spec: Local SSD disk (https://cloud.google.com/compute/docs/disks/local-ssd). Default is "pd-standard".' type: string + encryptionKey: + description: EncryptionKey defines the KMS key to be + used to encrypt the disk. + properties: + keyType: + description: KeyType is the type of encryption key. + Must be either Managed, aka Customer-Managed Encryption + Key (CMEK) or Supplied, aka Customer-Supplied + EncryptionKey (CSEK). + enum: + - Managed + - Supplied + type: string + kmsKeyServiceAccount: + description: 'KMSKeyServiceAccount is the service + account being used for the encryption request + for the given KMS key. If absent, the Compute + Engine default service account is used. For example: + "kmsKeyServiceAccount": "name@project_id.iam.gserviceaccount.com. + The maximum length is based on the Service Account + ID (max 30), Project (max 30), and a valid gcloud + email suffix ("iam.gserviceaccount.com").' + maxLength: 85 + pattern: '[-_[A-Za-z0-9]+@[-_[A-Za-z0-9]+.iam.gserviceaccount.com' + type: string + managedKey: + description: ManagedKey references keys managed + by the Cloud Key Management Service. This should + be set when KeyType is Managed. + properties: + kmsKeyName: + description: 'KMSKeyName is the name of the + encryption key that is stored in Google Cloud + KMS. For example: "kmsKeyName": "projects/kms_project_id/locations/region/keyRings/key_region/cryptoKeys/key' + maxLength: 160 + pattern: projects\/[-_[A-Za-z0-9]+\/locations\/[-_[A-Za-z0-9]+\/keyRings\/[-_[A-Za-z0-9]+\/cryptoKeys\/[-_[A-Za-z0-9]+ + type: string + type: object + suppliedKey: + description: SuppliedKey provides the key used to + create or manage a disk. This should be set when + KeyType is Managed. + maxProperties: 1 + minProperties: 1 + properties: + rawKey: + description: 'RawKey specifies a 256-bit customer-supplied + encryption key, encoded in RFC 4648 base64 + to either encrypt or decrypt this resource. + You can provide either the rawKey or the rsaEncryptedKey. + For example: "rawKey": "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="' + format: byte + type: string + rsaEncryptedKey: + description: 'RSAEncryptedKey specifies an RFC + 4648 base64 encoded, RSA-wrapped 2048-bit + customer-supplied encryption key to either + encrypt or decrypt this resource. You can + provide either the rawKey or the rsaEncryptedKey. + For example: "rsaEncryptedKey": "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHi + z0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDi + D6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==" + The key must meet the following requirements + before you can provide it to Compute Engine: + 1. The key is wrapped using a RSA public key + certificate provided by Google. 2. After being + wrapped, the key must be encoded in RFC 4648 + base64 encoding. Gets the RSA public key certificate + provided by Google at: https://cloud-certs.storage.googleapis.com/google-cloud-csek-ingress.pem' + format: byte + type: string + type: object + required: + - keyType + type: object size: description: Size is the size of the disk in GBs. Defaults to 30GB. For "local-ssd" size is always 375GB. @@ -242,6 +317,80 @@ spec: (HDD) persistent disk 2. "pd-ssd" - SSD persistent disk Default is "pd-standard".' type: string + rootDiskEncryptionKey: + description: RootDiskEncryptionKey defines the KMS key to + be used to encrypt the root disk. + properties: + keyType: + description: KeyType is the type of encryption key. Must + be either Managed, aka Customer-Managed Encryption Key + (CMEK) or Supplied, aka Customer-Supplied EncryptionKey + (CSEK). + enum: + - Managed + - Supplied + type: string + kmsKeyServiceAccount: + description: 'KMSKeyServiceAccount is the service account + being used for the encryption request for the given + KMS key. If absent, the Compute Engine default service + account is used. For example: "kmsKeyServiceAccount": + "name@project_id.iam.gserviceaccount.com. The maximum + length is based on the Service Account ID (max 30), + Project (max 30), and a valid gcloud email suffix ("iam.gserviceaccount.com").' + maxLength: 85 + pattern: '[-_[A-Za-z0-9]+@[-_[A-Za-z0-9]+.iam.gserviceaccount.com' + type: string + managedKey: + description: ManagedKey references keys managed by the + Cloud Key Management Service. This should be set when + KeyType is Managed. + properties: + kmsKeyName: + description: 'KMSKeyName is the name of the encryption + key that is stored in Google Cloud KMS. For example: + "kmsKeyName": "projects/kms_project_id/locations/region/keyRings/key_region/cryptoKeys/key' + maxLength: 160 + pattern: projects\/[-_[A-Za-z0-9]+\/locations\/[-_[A-Za-z0-9]+\/keyRings\/[-_[A-Za-z0-9]+\/cryptoKeys\/[-_[A-Za-z0-9]+ + type: string + type: object + suppliedKey: + description: SuppliedKey provides the key used to create + or manage a disk. This should be set when KeyType is + Managed. + maxProperties: 1 + minProperties: 1 + properties: + rawKey: + description: 'RawKey specifies a 256-bit customer-supplied + encryption key, encoded in RFC 4648 base64 to either + encrypt or decrypt this resource. You can provide + either the rawKey or the rsaEncryptedKey. For example: + "rawKey": "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0="' + format: byte + type: string + rsaEncryptedKey: + description: 'RSAEncryptedKey specifies an RFC 4648 + base64 encoded, RSA-wrapped 2048-bit customer-supplied + encryption key to either encrypt or decrypt this + resource. You can provide either the rawKey or the + rsaEncryptedKey. For example: "rsaEncryptedKey": + "ieCx/NcW06PcT7Ep1X6LUTc/hLvUDYyzSZPPVCVPTVEohpeHASqC8uw5TzyO9U+Fka9JFHi + z0mBibXUInrC/jEk014kCK/NPjYgEMOyssZ4ZINPKxlUh2zn1bV+MCaTICrdmuSBTWlUUiFoDi + D6PYznLwh8ZNdaheCeZ8ewEXgFQ8V+sDroLaN3Xs3MDTXQEMMoNUXMCZEIpg9Vtp9x2oe==" + The key must meet the following requirements before + you can provide it to Compute Engine: 1. The key + is wrapped using a RSA public key certificate provided + by Google. 2. After being wrapped, the key must + be encoded in RFC 4648 base64 encoding. Gets the + RSA public key certificate provided by Google at: + https://cloud-certs.storage.googleapis.com/google-cloud-csek-ingress.pem' + format: byte + type: string + type: object + required: + - keyType + type: object serviceAccounts: description: 'ServiceAccount specifies the service account email and which scopes to assign to the machine. Defaults