diff --git a/cli/internal/cloudcmd/tfvars.go b/cli/internal/cloudcmd/tfvars.go index aab752aca..cde3dc82f 100644 --- a/cli/internal/cloudcmd/tfvars.go +++ b/cli/internal/cloudcmd/tfvars.go @@ -231,6 +231,7 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste InternalLoadBalancer: conf.InternalLoadBalancer, CCTechnology: ccTech, AdditionalLabels: conf.Tags, + IAMServiceAccountVM: conf.Provider.GCP.IAMServiceAccountVM, } } diff --git a/cli/internal/cmd/apply_test.go b/cli/internal/cmd/apply_test.go index 17c03f33f..6988da6cb 100644 --- a/cli/internal/cmd/apply_test.go +++ b/cli/internal/cmd/apply_test.go @@ -256,6 +256,7 @@ func TestValidateInputs(t *testing.T) { ClientX509CertURL: "client_cert", })) cfg.Provider.GCP.ServiceAccountKeyPath = "saKey.json" + cfg.Provider.GCP.IAMServiceAccountVM = "example@example.com" } require.NoError(fh.WriteYAML(constants.ConfigFilename, cfg)) diff --git a/cli/internal/cmd/iamcreategcp.go b/cli/internal/cmd/iamcreategcp.go index f7ff3f62d..72e911371 100644 --- a/cli/internal/cmd/iamcreategcp.go +++ b/cli/internal/cmd/iamcreategcp.go @@ -141,11 +141,12 @@ func (c *gcpIAMCreator) printOutputValues(cmd *cobra.Command, _ cloudcmd.IAMOutp cmd.Printf("serviceAccountKeyPath:\t%s\n\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) } -func (c *gcpIAMCreator) writeOutputValuesToConfig(conf *config.Config, _ cloudcmd.IAMOutput) { +func (c *gcpIAMCreator) writeOutputValuesToConfig(conf *config.Config, out cloudcmd.IAMOutput) { conf.Provider.GCP.Project = c.flags.projectID conf.Provider.GCP.ServiceAccountKeyPath = constants.GCPServiceAccountKeyFilename // File was created in workspace, so only the filename is needed. conf.Provider.GCP.Region = c.flags.region conf.Provider.GCP.Zone = c.flags.zone + conf.Provider.GCP.IAMServiceAccountVM = out.GCPOutput.IAMServiceAccountVM for groupName, group := range conf.NodeGroups { group.Zone = c.flags.zone conf.NodeGroups[groupName] = group diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 568c31ff8..524092783 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -539,6 +539,7 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs conf.Provider.GCP.Project = "test-project" conf.Provider.GCP.Zone = "test-zone" conf.Provider.GCP.ServiceAccountKeyPath = "test-key-path" + conf.Provider.GCP.IAMServiceAccountVM = "example@example.com" conf.Attestation.GCPSEVSNP.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce, measurements.PCRMeasurementLength) conf.Attestation.GCPSEVSNP.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce, measurements.PCRMeasurementLength) conf.Attestation.GCPSEVSNP.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce, measurements.PCRMeasurementLength) diff --git a/cli/internal/terraform/variables.go b/cli/internal/terraform/variables.go index 84ffcab5f..e6dd0313c 100644 --- a/cli/internal/terraform/variables.go +++ b/cli/internal/terraform/variables.go @@ -141,6 +141,8 @@ type GCPClusterVariables struct { InternalLoadBalancer bool `hcl:"internal_load_balancer" cty:"internal_load_balancer"` // CCTechnology is the confidential computing technology to use on the VMs. (`SEV` or `SEV_SNP`) CCTechnology string `hcl:"cc_technology" cty:"cc_technology"` + // IAMServiceAccountControlPlane is the IAM service account mail address to attach to VMs. + IAMServiceAccountVM string `hcl:"iam_service_account_vm" cty:"iam_service_account_vm"` // AdditionalLables are (optional) additional labels that should be applied to created resources. AdditionalLabels cloudprovider.Tags `hcl:"additional_labels" cty:"additional_labels"` } diff --git a/cli/internal/terraform/variables_test.go b/cli/internal/terraform/variables_test.go index 02567c314..c32b4e7a7 100644 --- a/cli/internal/terraform/variables_test.go +++ b/cli/internal/terraform/variables_test.go @@ -122,8 +122,9 @@ func TestGCPClusterVariables(t *testing.T) { DiskType: "pd-ssd", }, }, - CustomEndpoint: "example.com", - CCTechnology: "SEV_SNP", + CustomEndpoint: "example.com", + CCTechnology: "SEV_SNP", + IAMServiceAccountVM: "example@example.com", } // test that the variables are correctly rendered @@ -151,10 +152,11 @@ node_groups = { zone = "eu-central-1b" } } -custom_endpoint = "example.com" -internal_load_balancer = false -cc_technology = "SEV_SNP" -additional_labels = null +custom_endpoint = "example.com" +internal_load_balancer = false +cc_technology = "SEV_SNP" +iam_service_account_vm = "example@example.com" +additional_labels = null ` got := vars.String() assert.Equal(t, strings.Fields(want), strings.Fields(got)) // to ignore whitespace differences @@ -162,10 +164,11 @@ additional_labels = null func TestGCPIAMVariables(t *testing.T) { vars := GCPIAMVariables{ - Project: "my-project", - Region: "eu-central-1", - Zone: "eu-central-1a", - ServiceAccountID: "my-service-account", + Project: "my-project", + Region: "eu-central-1", + Zone: "eu-central-1a", + ServiceAccountID: "my-service-account", + IAMServiceAccountVM: "my-service-account-vm", } // test that the variables are correctly rendered @@ -173,6 +176,7 @@ func TestGCPIAMVariables(t *testing.T) { region = "eu-central-1" zone = "eu-central-1a" service_account_id = "my-service-account" +service_account_id_vm = "my-service-account-vm" ` got := vars.String() assert.Equal(t, strings.Fields(want), strings.Fields(got)) // to ignore whitespace differences diff --git a/internal/config/config.go b/internal/config/config.go index 5aefb05b3..6e6d1511a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -188,6 +188,9 @@ type GCPConfig struct { // Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization ServiceAccountKeyPath string `yaml:"serviceAccountKeyPath" validate:"required"` // description: | + // GCP service account mail address. This is being attached to the VMs for authorization. + IAMServiceAccountVM string `yaml:"IAMServiceAccountVM" validate:"required"` + // description: | // Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"` // description: | @@ -349,6 +352,7 @@ func Default() *Config { Region: "", Zone: "", ServiceAccountKeyPath: "", + IAMServiceAccountVM: "", DeployCSIDriver: toPtr(true), UseMarketplaceImage: toPtr(false), }, diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index d26af1643..b87db6b86 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -241,7 +241,7 @@ func init() { FieldName: "gcp", }, } - GCPConfigDoc.Fields = make([]encoder.Doc, 6) + GCPConfigDoc.Fields = make([]encoder.Doc, 7) GCPConfigDoc.Fields[0].Name = "project" GCPConfigDoc.Fields[0].Type = "string" GCPConfigDoc.Fields[0].Note = "" @@ -262,16 +262,21 @@ func init() { GCPConfigDoc.Fields[3].Note = "" GCPConfigDoc.Fields[3].Description = "Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization" GCPConfigDoc.Fields[3].Comments[encoder.LineComment] = "Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization" - GCPConfigDoc.Fields[4].Name = "deployCSIDriver" - GCPConfigDoc.Fields[4].Type = "bool" + GCPConfigDoc.Fields[4].Name = "IAMServiceAccountVM" + GCPConfigDoc.Fields[4].Type = "string" GCPConfigDoc.Fields[4].Note = "" - GCPConfigDoc.Fields[4].Description = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage" - GCPConfigDoc.Fields[4].Comments[encoder.LineComment] = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage" - GCPConfigDoc.Fields[5].Name = "useMarketplaceImage" + GCPConfigDoc.Fields[4].Description = "GCP service account mail address. This is being attached to the VMs for authorization." + GCPConfigDoc.Fields[4].Comments[encoder.LineComment] = "GCP service account mail address. This is being attached to the VMs for authorization." + GCPConfigDoc.Fields[5].Name = "deployCSIDriver" GCPConfigDoc.Fields[5].Type = "bool" GCPConfigDoc.Fields[5].Note = "" - GCPConfigDoc.Fields[5].Description = "Use the specified GCP Marketplace image offering." - GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "Use the specified GCP Marketplace image offering." + GCPConfigDoc.Fields[5].Description = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage" + GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage" + GCPConfigDoc.Fields[6].Name = "useMarketplaceImage" + GCPConfigDoc.Fields[6].Type = "bool" + GCPConfigDoc.Fields[6].Note = "" + GCPConfigDoc.Fields[6].Description = "Use the specified GCP Marketplace image offering." + GCPConfigDoc.Fields[6].Comments[encoder.LineComment] = "Use the specified GCP Marketplace image offering." OpenStackConfigDoc.Type = "OpenStackConfig" OpenStackConfigDoc.Comments[encoder.LineComment] = "OpenStackConfig holds config information for OpenStack based Constellation deployments." diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5c60b26dc..e58cf6bb0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -328,10 +328,10 @@ func TestFromFile(t *testing.T) { } func TestValidate(t *testing.T) { - const defaultErrCount = 33 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default + const defaultErrCount = 34 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default const azErrCount = 7 const awsErrCount = 8 - const gcpErrCount = 8 + const gcpErrCount = 9 // TODO(AB#3132,3u13r): refactor config validation tests // Note that the `cnf.Image = ""` is a hack to align `bazel test` with `go test` behavior @@ -464,6 +464,7 @@ func TestValidate(t *testing.T) { gcp.Project = "test-project" gcp.Zone = "test-zone" gcp.ServiceAccountKeyPath = "test-key-path" + gcp.IAMServiceAccountVM = "example@example.com" cnf.Provider = ProviderConfig{} cnf.Provider.GCP = gcp cnf.Attestation.GCPSEVSNP.Measurements = measurements.M{ diff --git a/terraform-provider-constellation/examples/full/gcp/main.tf b/terraform-provider-constellation/examples/full/gcp/main.tf index 7a9175af0..527aefd7c 100644 --- a/terraform-provider-constellation/examples/full/gcp/main.tf +++ b/terraform-provider-constellation/examples/full/gcp/main.tf @@ -81,6 +81,7 @@ module "gcp_infrastructure" { project = local.project_id internal_load_balancer = false cc_technology = local.cc_technology + iam_service_account_vm = module.gcp_iam.service_account_mail_vm } data "constellation_attestation" "foo" { diff --git a/terraform/infrastructure/gcp/main.tf b/terraform/infrastructure/gcp/main.tf index 19275bb06..fdcd93e0e 100644 --- a/terraform/infrastructure/gcp/main.tf +++ b/terraform/infrastructure/gcp/main.tf @@ -160,28 +160,29 @@ resource "google_compute_firewall" "firewall_internal_pods" { } module "instance_group" { - source = "./modules/instance_group" - for_each = var.node_groups - base_name = local.name - node_group_name = each.key - role = each.value.role - zone = each.value.zone - uid = local.uid - instance_type = each.value.instance_type - initial_count = each.value.initial_count - image_id = var.image_id - disk_size = each.value.disk_size - disk_type = each.value.disk_type - network = google_compute_network.vpc_network.id - subnetwork = google_compute_subnetwork.vpc_subnetwork.id - alias_ip_range_name = google_compute_subnetwork.vpc_subnetwork.secondary_ip_range[0].range_name - kube_env = local.kube_env - debug = var.debug - named_ports = each.value.role == "control-plane" ? local.control_plane_named_ports : [] - labels = local.labels - init_secret_hash = local.init_secret_hash - custom_endpoint = var.custom_endpoint - cc_technology = var.cc_technology + source = "./modules/instance_group" + for_each = var.node_groups + base_name = local.name + node_group_name = each.key + role = each.value.role + zone = each.value.zone + uid = local.uid + instance_type = each.value.instance_type + initial_count = each.value.initial_count + image_id = var.image_id + disk_size = each.value.disk_size + disk_type = each.value.disk_type + network = google_compute_network.vpc_network.id + subnetwork = google_compute_subnetwork.vpc_subnetwork.id + alias_ip_range_name = google_compute_subnetwork.vpc_subnetwork.secondary_ip_range[0].range_name + kube_env = local.kube_env + debug = var.debug + named_ports = each.value.role == "control-plane" ? local.control_plane_named_ports : [] + labels = local.labels + init_secret_hash = local.init_secret_hash + custom_endpoint = var.custom_endpoint + cc_technology = var.cc_technology + iam_service_account_vm = var.iam_service_account_vm } resource "google_compute_address" "loadbalancer_ip_internal" { diff --git a/terraform/infrastructure/gcp/modules/instance_group/main.tf b/terraform/infrastructure/gcp/modules/instance_group/main.tf index 2ba833bbb..93a9e1368 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/main.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/main.tf @@ -77,17 +77,11 @@ resource "google_compute_instance_template" "template" { on_host_maintenance = "TERMINATE" } + # Define all IAM access via the service account and not via scopes: + # See: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance_template#nested_service_account service_account { - scopes = [ - "https://www.googleapis.com/auth/compute", - "https://www.googleapis.com/auth/servicecontrol", - "https://www.googleapis.com/auth/service.management.readonly", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/logging.write", - "https://www.googleapis.com/auth/monitoring.write", - "https://www.googleapis.com/auth/trace.append", - "https://www.googleapis.com/auth/cloud-platform", - ] + email = var.iam_service_account_vm + scopes = ["cloud-platform"] } shielded_instance_config { diff --git a/terraform/infrastructure/gcp/modules/instance_group/variables.tf b/terraform/infrastructure/gcp/modules/instance_group/variables.tf index 5370ec7d1..e4d2cbe5c 100644 --- a/terraform/infrastructure/gcp/modules/instance_group/variables.tf +++ b/terraform/infrastructure/gcp/modules/instance_group/variables.tf @@ -108,3 +108,9 @@ variable "cc_technology" { error_message = "The confidential computing technology has to be 'SEV' or 'SEV_SNP'." } } + +variable "iam_service_account_vm" { + type = string + default = "" + description = "IAM service account used for the VMs" +} diff --git a/terraform/infrastructure/gcp/variables.tf b/terraform/infrastructure/gcp/variables.tf index 601394a55..0768dd242 100644 --- a/terraform/infrastructure/gcp/variables.tf +++ b/terraform/infrastructure/gcp/variables.tf @@ -75,3 +75,9 @@ variable "additional_labels" { default = {} description = "Additional labels that should be given to created recources." } + +variable "iam_service_account_vm" { + type = string + default = "" + description = "IAM service account used for the VMs" +}