diff --git a/infrastructure/environments/dev/variables.sh b/infrastructure/environments/dev/variables.sh index 69d538e..aeff280 100644 --- a/infrastructure/environments/dev/variables.sh +++ b/infrastructure/environments/dev/variables.sh @@ -4,3 +4,4 @@ AZURE_SUBSCRIPTION="Breast Screening - NBSS Extraction - Dev" HUB_SUBSCRIPTION="Digital Screening DToS - DevOps" ENABLE_SOFT_DELETE=false TERRAFORM_MODULES_REF=main +HUB=dev diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index bb336ec..4f43379 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -1,34 +1,96 @@ -resource "azapi_resource" "defender_storage" { - for_each = local.defender_storage_accounts +locals { + defender_upload_storage_account_name = substr(replace("sa${var.app_short_name}${var.environment}defender", "/[^0-9a-z]/", ""), 0, 24) + + defender_containers = [ + "clean-scans", + "quarantine-scans" + ] + + defender_target_storage_accounts = { + (local.upload_storage_account_name) = azurerm_storage_account.upload.id + } +} + +# Configure Defender for all trust upload storage accounts +# Use "update" to modify existing Defender for Storage settings rather than creating new ones +resource "azapi_update_resource" "defender_settings" { + for_each = local.defender_target_storage_accounts type = "Microsoft.Security/defenderForStorageSettings@2026-01-01-preview" - name = "current" - parent_id = each.value + resource_id = "${each.value}/providers/Microsoft.Security/defenderForStorageSettings/current" body = { properties = { - isEnabled = var.scan_is_enabled + isEnabled = var.scan_is_enabled - # only provide if the storage accounts absolutely must have their own settings - overrideSubscriptionLevelSettings = var.override_subscription_settings_enabled + # only provide if the storage accounts absolutely must have their own settings + overrideSubscriptionLevelSettings = var.override_subscription_settings_enabled - malwareScanning = { + sensitiveDataDiscovery = { + isEnabled = var.sensitive_data_discovery_enabled && var.scan_is_enabled + } + + malwareScanning = { blobScanResultsOptions = "BlobIndexTags" onUpload = { isEnabled = var.malware_scanning_on_upload_enabled && var.scan_is_enabled capGBPerMonth = var.malware_scanning_on_upload_cap_gb_per_month } } - - sensitiveDataDiscovery = { - isEnabled = var.sensitive_data_discovery_enabled && var.scan_is_enabled - } } } } -locals { - defender_storage_accounts = { - upload = azurerm_storage_account.upload.id +# for the incoming function that triggers a move from the upload container to the defender storage account containers +# we need to ensure that the defender storage accounts are properly configured and accessible +# Since Defender storage accounts are not publicly accessible, we don't need to worry about write-only +# permissions as we do with the client landing containers + +resource "azurerm_storage_account" "defender_storage_account" { + name = local.defender_upload_storage_account_name + resource_group_name = azurerm_resource_group.deploy_resource_group.name + location = azurerm_resource_group.deploy_resource_group.location + + account_tier = "Standard" + account_replication_type = "LRS" + account_kind = "StorageV2" + + allow_nested_items_to_be_public = false + cross_tenant_replication_enabled = false + default_to_oauth_authentication = true + https_traffic_only_enabled = true + infrastructure_encryption_enabled = true + min_tls_version = "TLS1_2" + + public_network_access_enabled = false + + # The intention for the storage account is to provide Shared Key access and also Entra ID authentication. + shared_access_key_enabled = true +} + +resource "azurerm_storage_container" "defender_containers" { + for_each = toset(local.defender_containers) + name = each.key + storage_account_id = azurerm_storage_account.defender_storage_account.id + container_access_type = "private" +} + +resource "azurerm_private_endpoint" "defender_storage_blob" { + provider = azurerm.hub + name = "${local.defender_upload_storage_account_name}-pep" + location = data.azurerm_resource_group.hub_private_endpoint.location + resource_group_name = data.azurerm_resource_group.hub_private_endpoint.name + subnet_id = data.azurerm_subnet.hub_private_endpoint.id + + private_service_connection { + name = "${local.defender_upload_storage_account_name}-blob" + private_connection_resource_id = azurerm_storage_account.defender_storage_account.id + subresource_names = ["blob"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${local.defender_upload_storage_account_name}-blob-dns" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blob.id] } } diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf index 4821fb9..1eaac09 100644 --- a/infrastructure/terraform/main.tf +++ b/infrastructure/terraform/main.tf @@ -12,7 +12,7 @@ module "shared_config" { application = var.app_short_name } -locals{ - resource_group_name = "rg-${var.app_short_name}-${var.env_config}-uks" - region = "uksouth" +locals { + resource_group_name = "rg-${var.app_short_name}-${var.env_config}-uks" + region = "uksouth" } diff --git a/infrastructure/terraform/networking.tf b/infrastructure/terraform/networking.tf new file mode 100644 index 0000000..991f149 --- /dev/null +++ b/infrastructure/terraform/networking.tf @@ -0,0 +1,41 @@ +locals { + hub_private_endpoint_resource_group_name = "rg-hub-${var.hub}-uks-hub-private-endpoints" + hub_private_dns_zone_resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" + hub_network_resource_group_name = "rg-hub-${var.hub}-uks-hub-networking" + hub_vnet_name = "VNET-${upper(var.hub)}-UKS-HUB" + hub_private_endpoint_subnet_name = "SN-${upper(var.hub)}-UKS-HUB-pep" +} + +data "azurerm_resource_group" "hub_private_endpoint" { + provider = azurerm.hub + name = local.hub_private_endpoint_resource_group_name +} + +data "azurerm_resource_group" "hub_private_dns_zones" { + provider = azurerm.hub + name = local.hub_private_dns_zone_resource_group_name +} + +data "azurerm_resource_group" "hub_network" { + provider = azurerm.hub + name = local.hub_network_resource_group_name +} + +data "azurerm_virtual_network" "hub" { + provider = azurerm.hub + name = local.hub_vnet_name + resource_group_name = data.azurerm_resource_group.hub_network.name +} + +data "azurerm_subnet" "hub_private_endpoint" { + provider = azurerm.hub + name = local.hub_private_endpoint_subnet_name + virtual_network_name = data.azurerm_virtual_network.hub.name + resource_group_name = data.azurerm_resource_group.hub_network.name +} + +data "azurerm_private_dns_zone" "blob" { + provider = azurerm.hub + name = var.storage_blob_private_dns_zone_name + resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" +} diff --git a/infrastructure/terraform/output.tf b/infrastructure/terraform/output.tf index 2b6e4fa..b63881f 100644 --- a/infrastructure/terraform/output.tf +++ b/infrastructure/terraform/output.tf @@ -1,6 +1,26 @@ -output "upload_container_urls" { - value = { +locals { + upload_container_details = { for container_key, container in azurerm_storage_container.bso : - container_key => "${azurerm_storage_account.upload.primary_blob_endpoint}${container.name}" + container_key => { + endpoint = container.id + storage_account_id = container.storage_account_id + } + } + + upload_container_urls = { + for container_key, container in local.upload_container_details : + container_key => container.endpoint } } + +output "upload_container_urls" { + value = local.upload_container_urls +} + +output "defender_storage_account_id" { + value = azurerm_storage_account.defender_storage_account.id +} + +output "defender_storage_private_endpoint_id" { + value = azurerm_private_endpoint.defender_storage_blob.id +} diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf index c5d5040..1a6e294 100644 --- a/infrastructure/terraform/variables.tf +++ b/infrastructure/terraform/variables.tf @@ -16,6 +16,10 @@ variable "environment" { variable "hub" { description = "Hub name (dev or prod)" type = string + validation { + condition = contains(["dev", "prod"], var.hub) + error_message = "Hub must be either 'dev' or 'prod'" + } } variable "hub_subscription_id"{ @@ -28,12 +32,6 @@ variable "arm_subscription_id" { type = string } -variable "enable_defender_for_storage" { - description = "Enable Microsoft Defender for Storage for the configured storage account and containers" - type = bool - default = false -} - # defender for storage variables variable "override_subscription_settings_enabled" { @@ -45,7 +43,7 @@ variable "override_subscription_settings_enabled" { variable "malware_scanning_on_upload_enabled" { description = "Enable malware scanning on upload for Microsoft Defender for Storage" type = bool - default = true + default = false } variable "malware_scanning_on_upload_cap_gb_per_month" { @@ -65,3 +63,9 @@ variable "scan_is_enabled" { type = bool default = true } + +variable "storage_blob_private_dns_zone_name" { + description = "Private DNS zone name for storage blob private endpoints" + type = string + default = "privatelink.blob.core.windows.net" +}