diff --git a/.github/workflows/standalone-azuread.json b/.github/workflows/standalone-azuread.json index 6dc3613c36..950bcdeff5 100644 --- a/.github/workflows/standalone-azuread.json +++ b/.github/workflows/standalone-azuread.json @@ -11,6 +11,8 @@ "azuread/104-azuread-group-membership", "azuread/105-azuread-application-with-optional-claims", "azuread/106-azuread-application-with-api-scopes", + "azuread/201-groups-and-roles", + "azuread/202-azuread-application-federated-credentials" "azuread/108-azuread-application-with-app-roles", "azuread/201-groups-and-roles" ] diff --git a/.github/workflows/standalone-compute.json b/.github/workflows/standalone-compute.json index a1effb7e50..d70272d48b 100644 --- a/.github/workflows/standalone-compute.json +++ b/.github/workflows/standalone-compute.json @@ -50,6 +50,7 @@ "compute/virtual_machine/215-vm-keyvault-for-windows-extension", "compute/virtual_machine/216-vm-linux_diagnostic_extensions", "compute/virtual_machine/217-vm-disk-encryption-set-msi", - "compute/vmware_cluster/101-vmware_cluster" + "compute/vmware_cluster/101-vmware_cluster", + "compute/kubernetes_services/109-single-cluster-federated-credentials" ] } diff --git a/.github/workflows/standalone-scenarios.json b/.github/workflows/standalone-scenarios.json index 2bd6a3da02..34bc2b5db5 100644 --- a/.github/workflows/standalone-scenarios.json +++ b/.github/workflows/standalone-scenarios.json @@ -137,6 +137,7 @@ "webapps/function_app/101-function_app-private", "webapps/function_app/102-function_app-linux", "webapps/function_app/103-function_app-windows", - "webapps/static_site/101-simple-static-web-app" + "webapps/static_site/101-simple-static-web-app", + "managed_service_identity/101-mi-federated_credential" ] } diff --git a/azuread_federated_credentials.tf b/azuread_federated_credentials.tf new file mode 100644 index 0000000000..8126529779 --- /dev/null +++ b/azuread_federated_credentials.tf @@ -0,0 +1,12 @@ +module "azuread_federated_credentials" { + source = "./modules/azuread/federated_credentials/" + for_each = local.azuread.azuread_federated_credentials + depends_on = [module.azuread_applications_v1] + client_config = local.client_config + settings = each.value + azuread_applications = local.combined_objects_azuread_applications +} + +output "azuread_federated_credentials" { + value = module.azuread_federated_credentials +} diff --git a/compute_aks_clusters.tf b/compute_aks_clusters.tf index aa989af4c9..906bb5449b 100644 --- a/compute_aks_clusters.tf +++ b/compute_aks_clusters.tf @@ -14,6 +14,8 @@ module "aks_clusters" { managed_identities = local.combined_objects_managed_identities settings = each.value vnets = local.combined_objects_networking + azuread_applications = local.combined_objects_azuread_applications + private_endpoints = try(each.value.private_endpoints, {}) admin_group_object_ids = try(each.value.admin_groups.azuread_group_keys, null) == null ? null : try( each.value.admin_groups.ids, diff --git a/examples/azuread/202-azuread-application-federated-credentials/configuration.tfvars b/examples/azuread/202-azuread-application-federated-credentials/configuration.tfvars new file mode 100644 index 0000000000..d6ab21993a --- /dev/null +++ b/examples/azuread/202-azuread-application-federated-credentials/configuration.tfvars @@ -0,0 +1,24 @@ +azuread_applications = { + aks_auth_app = { + application_name = "app-najeeb-sandbox-aksadmin" + } +} + +azuread_federated_credentials = { + cred1 = { + display_name = "app-wi-fed01" + subject = "system:serviceaccount:demo:workload-identity-sa" + oidc_issuer_url = "https://westeurope.oic.prod-aks.azure.com/" + azuread_application = { + key = "aks_auth_app" + } + } +} + +azuread_service_principals = { + aks_auth_app = { + azuread_application = { + key = "aks_auth_app" + } + } +} \ No newline at end of file diff --git a/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/aks.tfvars b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/aks.tfvars new file mode 100644 index 0000000000..09845aacc5 --- /dev/null +++ b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/aks.tfvars @@ -0,0 +1,129 @@ +global_settings = { + default_region = "region1" + regions = { + region1 = "australiaeast" + } +} + +resource_groups = { + aks_re1 = { + name = "aks-re1" + region = "region1" + } + msi_region1 = { + name = "security-rg1" + region = "region1" + } +} + +aks_clusters = { + cluster_re1 = { + name = "akscluster-re1-001" + resource_group_key = "aks_re1" + os_type = "Linux" + + identity = { + type = "SystemAssigned" + } + + vnet_key = "spoke_aks_re1" + + network_profile = { + network_plugin = "azure" + load_balancer_sku = "standard" + } + + # enable_rbac = true + role_based_access_control = { + enabled = true + azure_active_directory = { + managed = true + } + } + + oms_agent = { + log_analytics_key = "central_logs_region1" + } + + # admin_groups = { + # # ids = [] + # # azuread_groups = { + # # keys = [] + # # } + # } + + load_balancer_profile = { + # Only one option can be set + managed_outbound_ip_count = 1 + } + + default_node_pool = { + name = "sharedsvc" + vm_size = "Standard_F4s_v2" + #subnet_key = "aks_nodepool_system" + subnet = { + key = "aks_nodepool_system" + #resource_id = "/subscriptions/97958dac-xxxx-xxxx-xxxx-9f436fa73bd4/resourceGroups/qxgc-rg-aks-re1/providers/Microsoft.Network/virtualNetworks/qxgc-vnet-aks/subnets/qxgc-snet-aks_nodepool_system" + } + enabled_auto_scaling = false + enable_node_public_ip = false + max_pods = 30 + node_count = 1 + os_disk_size_gb = 512 + tags = { + "project" = "system services" + } + } + + node_resource_group_name = "aks-nodes-re1" + + addon_profile = { + azure_keyvault_secrets_provider = { + secret_rotation_enabled = true + secret_rotation_interval = "2m" + } + } + azuread_federated_credentials = { + cred1 = { + display_name = "app-wi-fed02" + subject = "system:serviceaccount:demo:workload-identity-sa" + azuread_application = { + key = "aks_auth_app" + #lz_key = "" + } + } + } + + mi_federated_credentials = { + cred1 = { + name = "mi-wi-demo02" + subject = "system:serviceaccount:demo:workload-identity-sa" + managed_identity = { + key = "workload_system_mi" + #lz_key = "" + } + } + } + } +} + +azuread_applications = { + aks_auth_app = { + application_name = "app-najeeb-sandbox-aksadmin" + } +} + +azuread_service_principals = { + aks_auth_app = { + azuread_application = { + key = "aks_auth_app" + } + } +} + +managed_identities = { + workload_system_mi = { + name = "demo-mi-wi" + resource_group_key = "msi_region1" + } +} diff --git a/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/diagnostics.tfvars b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/diagnostics.tfvars new file mode 100644 index 0000000000..6cef8d8694 --- /dev/null +++ b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/diagnostics.tfvars @@ -0,0 +1,7 @@ +diagnostic_log_analytics = { + central_logs_region1 = { + region = "region1" + name = "logs" + resource_group_key = "aks_re1" + } +} \ No newline at end of file diff --git a/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/networking.tfvars b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/networking.tfvars new file mode 100644 index 0000000000..b8ed1dda00 --- /dev/null +++ b/examples/compute/kubernetes_services/109-single-cluster-federated-credentials/networking.tfvars @@ -0,0 +1,190 @@ +vnets = { + spoke_aks_re1 = { + resource_group_key = "aks_re1" + region = "region1" + vnet = { + name = "aks" + address_space = ["100.64.48.0/22"] + } + specialsubnets = {} + subnets = { + aks_nodepool_system = { + name = "aks_nodepool_system" + cidr = ["100.64.48.0/24"] + nsg_key = "azure_kubernetes_cluster_nsg" + } + aks_nodepool_user1 = { + name = "aks_nodepool_user1" + cidr = ["100.64.49.0/24"] + nsg_key = "azure_kubernetes_cluster_nsg" + } + aks_nodepool_user2 = { + name = "aks_nodepool_user2" + cidr = ["100.64.50.0/24"] + nsg_key = "azure_kubernetes_cluster_nsg" + } + AzureBastionSubnet = { + name = "AzureBastionSubnet" #Must be called AzureBastionSubnet + cidr = ["100.64.51.64/27"] + nsg_key = "azure_bastion_nsg" + } + private_endpoints = { + name = "private_endpoints" + cidr = ["100.64.51.0/27"] + enforce_private_link_endpoint_network_policies = true + } + jumpbox = { + name = "jumpbox" + cidr = ["100.64.51.128/27"] + nsg_key = "azure_bastion_nsg" + } + } + + } +} + +network_security_group_definition = { + # This entry is applied to all subnets with no NSG defined + empty_nsg = {} + azure_kubernetes_cluster_nsg = { + nsg = [ + { + name = "aks-http-in-allow", + priority = "100" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "80" + source_address_prefix = "*" + destination_address_prefix = "*" + }, + { + name = "aks-https-in-allow", + priority = "110" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "*" + destination_address_prefix = "*" + }, + { + name = "aks-api-out-allow-1194", + priority = "100" + direction = "Outbound" + access = "Allow" + protocol = "Udp" + source_port_range = "*" + destination_port_range = "1194" + source_address_prefix = "*" + destination_address_prefix = "AzureCloud" + }, + { + name = "aks-api-out-allow-9000", + priority = "110" + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "9000" + source_address_prefix = "*" + destination_address_prefix = "AzureCloud" + }, + { + name = "aks-ntp-out-allow", + priority = "120" + direction = "Outbound" + access = "Allow" + protocol = "Udp" + source_port_range = "*" + destination_port_range = "123" + source_address_prefix = "*" + destination_address_prefix = "*" + }, + { + name = "aks-https-out-allow-443", + priority = "130" + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "*" + destination_address_prefix = "*" + }, + ] + } + azure_bastion_nsg = { + + nsg = [ + { + name = "bastion-in-allow", + priority = "100" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "*" + destination_address_prefix = "*" + }, + { + name = "bastion-control-in-allow-443", + priority = "120" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "135" + source_address_prefix = "GatewayManager" + destination_address_prefix = "*" + }, + { + name = "Kerberos-password-change", + priority = "121" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "4443" + source_address_prefix = "GatewayManager" + destination_address_prefix = "*" + }, + { + name = "bastion-vnet-out-allow-22", + priority = "103" + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = "*" + destination_address_prefix = "VirtualNetwork" + }, + { + name = "bastion-vnet-out-allow-3389", + priority = "101" + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "3389" + source_address_prefix = "*" + destination_address_prefix = "VirtualNetwork" + }, + { + name = "bastion-azure-out-allow", + priority = "120" + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "*" + destination_address_prefix = "AzureCloud" + } + ] + } +} \ No newline at end of file diff --git a/examples/managed_service_identity/101-mi-federated_credential/configuration.tfvars b/examples/managed_service_identity/101-mi-federated_credential/configuration.tfvars new file mode 100644 index 0000000000..4fa515f030 --- /dev/null +++ b/examples/managed_service_identity/101-mi-federated_credential/configuration.tfvars @@ -0,0 +1,27 @@ +resource_groups = { + msi_region1 = { + name = "security-rg1" + region = "region1" + } +} + +managed_identities = { + workload_system_mi = { + name = "demo-mi-wi" + resource_group_key = "msi_region1" + } +} + +mi_federated_credentials = { + cred1 = { + name = "mi-wi-demo01" + subject = "system:serviceaccount:demo:workload-identity-sa" + oidc_issuer_url = "https://westeurope.oic.prod-aks.azure.com/" + managed_identity = { + key = "workload_system_mi" + } + resource_group = { + key = "msi_region1" + } + } +} \ No newline at end of file diff --git a/local.remote_objects.tf b/local.remote_objects.tf index 76de44f2cd..ae7871aa99 100644 --- a/local.remote_objects.tf +++ b/local.remote_objects.tf @@ -91,6 +91,7 @@ locals { logic_app_standard = try(local.combined_objects_logic_app_standard, null) machine_learning = try(local.combined_objects_machine_learning, null) managed_identities = try(local.combined_objects_managed_identities, null) + mi_federated_credentials = try(local.combined_objects_mi_federated_credentials, null) monitor_action_groups = try(local.combined_objects_monitor_action_groups, null) mssql_databases = try(local.combined_objects_mssql_databases, null) mssql_elastic_pools = try(local.combined_objects_mssql_elastic_pools, null) diff --git a/locals.combined_objects.tf b/locals.combined_objects.tf index 8c2f27186a..a17b225dea 100644 --- a/locals.combined_objects.tf +++ b/locals.combined_objects.tf @@ -116,6 +116,7 @@ locals { combined_objects_maintenance_configuration = merge(tomap({ (local.client_config.landingzone_key) = module.maintenance_configuration }), lookup(var.remote_objects, "maintenance_configuration", {})) combined_objects_maintenance_assignment_virtual_machine = merge(tomap({ (local.client_config.landingzone_key) = module.maintenance_assignment_virtual_machine }), lookup(var.remote_objects, "maintenance_assignment_virtual_machine", {})) combined_objects_managed_identities = merge(tomap({ (local.client_config.landingzone_key) = module.managed_identities }), lookup(var.remote_objects, "managed_identities", {}), lookup(var.data_sources, "managed_identities", {})) + combined_objects_mi_federated_credentials = merge(tomap({ (local.client_config.landingzone_key) = module.mi_federated_credentials }), try(var.remote_objects.mi_federated_credentials, {})) combined_objects_maps_accounts = merge(tomap({ (local.client_config.landingzone_key) = module.maps_accounts }), lookup(var.remote_objects, "maps_accounts", {})) combined_objects_monitor_action_groups = merge(tomap({ (local.client_config.landingzone_key) = module.monitor_action_groups }), lookup(var.remote_objects, "monitor_action_groups", {}), lookup(var.data_sources, "monitor_action_groups", {})) combined_objects_mssql_databases = merge(tomap({ (local.client_config.landingzone_key) = module.mssql_databases }), lookup(var.remote_objects, "mssql_databases", {}), lookup(var.data_sources, "mssql_databases", {})) diff --git a/locals.tf b/locals.tf index e81350b46f..0ae4e4f8bc 100644 --- a/locals.tf +++ b/locals.tf @@ -25,6 +25,7 @@ locals { azuread_service_principal_passwords = try(var.azuread.azuread_service_principal_passwords, {}) azuread_service_principals = try(var.azuread.azuread_service_principals, {}) azuread_users = try(var.azuread.azuread_users, {}) + azuread_federated_credentials = try(var.azuread.azuread_federated_credentials, {}) } client_config = var.client_config == {} ? { diff --git a/mi_federated_credentials.tf b/mi_federated_credentials.tf new file mode 100644 index 0000000000..2d44cfee71 --- /dev/null +++ b/mi_federated_credentials.tf @@ -0,0 +1,13 @@ +module "mi_federated_credentials" { + source = "./modules/security/mi_federated_credentials/" + for_each = var.mi_federated_credentials + depends_on = [module.managed_identities] + client_config = local.client_config + resource_group = local.combined_objects_resource_groups[try(each.value.resource_group.lz_key, local.client_config.landingzone_key)][try(each.value.resource_group_key, each.value.resource_group.key)] + settings = each.value + managed_identities = local.combined_objects_managed_identities +} + +output "mi_federated_credentials" { + value = module.mi_federated_credentials +} diff --git a/modules/azuread/federated_credentials/federated_credential.tf b/modules/azuread/federated_credentials/federated_credential.tf new file mode 100644 index 0000000000..a5f8d4862b --- /dev/null +++ b/modules/azuread/federated_credentials/federated_credential.tf @@ -0,0 +1,8 @@ +resource "azuread_application_federated_identity_credential" "fed_cred" { + application_object_id = coalesce(try(var.settings.azuread_application.object_id, null), var.azuread_applications[try(var.settings.azuread_application.lz_key, var.client_config.landingzone_key)][var.settings.azuread_application.key].object_id) + display_name = var.settings.display_name + description = try(var.settings.description, null) + audiences = ["api://AzureADTokenExchange"] + issuer = coalesce(try(var.oidc_issuer_url, null), try(var.settings.oidc_issuer_url, null)) + subject = var.settings.subject +} \ No newline at end of file diff --git a/modules/azuread/federated_credentials/output.tf b/modules/azuread/federated_credentials/output.tf new file mode 100644 index 0000000000..3fed8cfd88 --- /dev/null +++ b/modules/azuread/federated_credentials/output.tf @@ -0,0 +1,3 @@ +output "credential_id" { + value = azuread_application_federated_identity_credential.fed_cred.id +} \ No newline at end of file diff --git a/modules/azuread/federated_credentials/variables.tf b/modules/azuread/federated_credentials/variables.tf new file mode 100644 index 0000000000..147c2418e4 --- /dev/null +++ b/modules/azuread/federated_credentials/variables.tf @@ -0,0 +1,12 @@ +variable "settings" { + default = {} +} +variable "client_config" { + description = "Client configuration object (see module README.md)." +} +variable "azuread_applications" { + default = {} +} +variable "oidc_issuer_url" { + default = null +} \ No newline at end of file diff --git a/modules/compute/aks/federated_credentials.tf b/modules/compute/aks/federated_credentials.tf new file mode 100644 index 0000000000..3b50714215 --- /dev/null +++ b/modules/compute/aks/federated_credentials.tf @@ -0,0 +1,20 @@ +module "azuread_federated_credentials" { + source = "../../azuread/federated_credentials" + for_each = var.settings.azuread_federated_credentials + + client_config = var.client_config + settings = each.value + azuread_applications = var.azuread_applications + oidc_issuer_url = azurerm_kubernetes_cluster.aks.oidc_issuer_url +} + +module "mi_federated_credentials" { + source = "../../security/mi_federated_credentials" + for_each = var.settings.mi_federated_credentials + + client_config = var.client_config + settings = each.value + managed_identities = var.managed_identities + oidc_issuer_url = azurerm_kubernetes_cluster.aks.oidc_issuer_url + resource_group = var.resource_group +} \ No newline at end of file diff --git a/modules/compute/aks/output.tf b/modules/compute/aks/output.tf index 4a0511459c..2f1318fb69 100644 --- a/modules/compute/aks/output.tf +++ b/modules/compute/aks/output.tf @@ -47,3 +47,7 @@ output "node_resource_group" { output "private_fqdn" { value = azurerm_kubernetes_cluster.aks.private_fqdn } + +output "oidc_issuer_url" { + value = azurerm_kubernetes_cluster.aks.oidc_issuer_url +} \ No newline at end of file diff --git a/modules/compute/aks/private_endpoint.tf b/modules/compute/aks/private_endpoint.tf index c76a587b00..fd6992b627 100644 --- a/modules/compute/aks/private_endpoint.tf +++ b/modules/compute/aks/private_endpoint.tf @@ -2,13 +2,13 @@ module "private_endpoint" { source = "../../networking/private_endpoint" for_each = var.private_endpoints - base_tags = local.tags + base_tags = var.global_settings.inherit_tags client_config = var.client_config global_settings = var.global_settings location = var.vnets[try(each.value.lz_key, var.client_config.landingzone_key)][try(each.value.vnet.key, each.value.vnet_key)].location name = each.value.name private_dns = var.private_dns - resource_group_name = var.resource_group_name + resource_group_name = local.resource_group_name resource_id = azurerm_kubernetes_cluster.aks.id settings = each.value subnet_id = can(each.value.subnet_id) ? each.value.subnet_id : var.vnets[try(each.value.lz_key, var.client_config.landingzone_key)][each.value.vnet_key].subnets[each.value.subnet_key].id diff --git a/modules/compute/aks/variables.tf b/modules/compute/aks/variables.tf index 71f67f3972..d90819a99b 100644 --- a/modules/compute/aks/variables.tf +++ b/modules/compute/aks/variables.tf @@ -38,4 +38,13 @@ variable "private_endpoints" { } variable "private_dns" { default = {} +} +variable "azuread_federated_credentials" { + default = {} +} +variable "mi_federated_credentials" { + default = {} +} +variable "azuread_applications" { + default = {} } \ No newline at end of file diff --git a/modules/security/mi_federated_credentials/federated_credential.tf b/modules/security/mi_federated_credentials/federated_credential.tf new file mode 100644 index 0000000000..9247cf41b6 --- /dev/null +++ b/modules/security/mi_federated_credentials/federated_credential.tf @@ -0,0 +1,8 @@ +resource "azurerm_federated_identity_credential" "fed_cred" { + name = var.settings.name + resource_group_name = coalesce(try(var.settings.resource_group.name, null), try(var.resource_group.name, null)) + audience = try(var.settings.audience, ["api://AzureADTokenExchange"]) + parent_id = coalesce(try(var.settings.managed_identity.id, null), var.managed_identities[try(var.settings.managed_identity.lz_key, var.client_config.landingzone_key)][var.settings.managed_identity.key].id) + subject = var.settings.subject + issuer = coalesce(try(var.oidc_issuer_url, null), try(var.settings.oidc_issuer_url, null)) +} \ No newline at end of file diff --git a/modules/security/mi_federated_credentials/output.tf b/modules/security/mi_federated_credentials/output.tf new file mode 100644 index 0000000000..9811216428 --- /dev/null +++ b/modules/security/mi_federated_credentials/output.tf @@ -0,0 +1,3 @@ +output "id" { + value = azurerm_federated_identity_credential.fed_cred.id +} \ No newline at end of file diff --git a/modules/security/mi_federated_credentials/variables.tf b/modules/security/mi_federated_credentials/variables.tf new file mode 100644 index 0000000000..3dd4fad283 --- /dev/null +++ b/modules/security/mi_federated_credentials/variables.tf @@ -0,0 +1,15 @@ +variable "settings" { + default = {} +} +variable "client_config" { + description = "Client configuration object (see module README.md)." +} +variable "resource_group" { + default = {} +} +variable "managed_identities" { + default = {} +} +variable "oidc_issuer_url" { + default = null +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index 4c4d69c4a1..9e019ec981 100644 --- a/variables.tf +++ b/variables.tf @@ -450,3 +450,6 @@ variable "load_test" { default = {} } +variable "mi_federated_credentials" { + default = {} +} \ No newline at end of file