From cc2559750b519aaf24b962593eaca11cb2155893 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:05:55 +0100 Subject: [PATCH 1/6] DSTA-683: create Defender target account and containers --- infrastructure/terraform/defender.tf | 54 +++++++++++++++++++++++++--- infrastructure/terraform/output.tf | 13 +++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index bb336ec..879f67f 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -1,5 +1,21 @@ +locals { + defender_upload_storage_account_name =substr(replace("sa${var.app_short_name}${var.environment}upload", "/[^0-9a-z]/", ""), 0, 24) + defender_containers = [ + "clean_scans", + "quarantine_scans" + ] + + defender_target_storage_accounts = { + for storage_account_id in toset([ + for container in values(local.upload_container_urls) : container.storage_account_id + ]) : + storage_account_id => storage_account_id + } +} + +# Turn on Defender for all trust upload storage accounts resource "azapi_resource" "defender_storage" { - for_each = local.defender_storage_accounts + for_each = local.defender_target_storage_accounts type = "Microsoft.Security/defenderForStorageSettings@2026-01-01-preview" name = "current" @@ -27,8 +43,36 @@ resource "azapi_resource" "defender_storage" { } } -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_scanned_uploads" { + 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" "scanned_uploads" { + for_each = toset(local.defender_containers) + name = each.key + storage_account_id = azurerm_storage_account.defender_scanned_uploads.id + container_access_type = "private" } diff --git a/infrastructure/terraform/output.tf b/infrastructure/terraform/output.tf index 2b6e4fa..4cf7965 100644 --- a/infrastructure/terraform/output.tf +++ b/infrastructure/terraform/output.tf @@ -1,6 +1,13 @@ -output "upload_container_urls" { - value = { +locals { + upload_container_urls = { 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 + } } } + +output "upload_container_urls" { + value = local.upload_container_urls +} From b86b875b226b1a12e70523a971313e568f1d2f8c Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:37:10 +0100 Subject: [PATCH 2/6] DSTA-683: improved naming and ddefaults and cleanup Update the reference to source accounts to use --- infrastructure/terraform/defender.tf | 43 +++++++++++++-------------- infrastructure/terraform/variables.tf | 8 +---- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index 879f67f..e635519 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -1,44 +1,43 @@ locals { - defender_upload_storage_account_name =substr(replace("sa${var.app_short_name}${var.environment}upload", "/[^0-9a-z]/", ""), 0, 24) + 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" + "clean-scans", + "quarantine-scans" ] defender_target_storage_accounts = { - for storage_account_id in toset([ - for container in values(local.upload_container_urls) : container.storage_account_id - ]) : - storage_account_id => storage_account_id + for container_key, container in local.upload_container_urls : + container_key => container.storage_account_id } } -# Turn on Defender for all trust upload storage accounts -resource "azapi_resource" "defender_storage" { +# 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 + sensitiveDataDiscovery = { + isEnabled = var.sensitive_data_discovery_enabled && var.scan_is_enabled + } - malwareScanning = { + 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 - } } } } @@ -48,7 +47,7 @@ resource "azapi_resource" "defender_storage" { # 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_scanned_uploads" { +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 @@ -70,9 +69,9 @@ resource "azurerm_storage_account" "defender_scanned_uploads" { shared_access_key_enabled = true } -resource "azurerm_storage_container" "scanned_uploads" { +resource "azurerm_storage_container" "defender_containers" { for_each = toset(local.defender_containers) name = each.key - storage_account_id = azurerm_storage_account.defender_scanned_uploads.id + storage_account_id = azurerm_storage_account.defender_storage_account.id container_access_type = "private" } diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf index c5d5040..bab41c7 100644 --- a/infrastructure/terraform/variables.tf +++ b/infrastructure/terraform/variables.tf @@ -28,12 +28,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 +39,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" { From ba820493cdf68698668920db97ee2850930f1951 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:33:25 +0100 Subject: [PATCH 3/6] DSTA-683: add private endpoint connectivity --- infrastructure/terraform/defender.tf | 20 +++++++++++++ infrastructure/terraform/networking.tf | 41 ++++++++++++++++++++++++++ infrastructure/terraform/output.tf | 8 +++++ infrastructure/terraform/variables.tf | 6 ++++ 4 files changed, 75 insertions(+) create mode 100644 infrastructure/terraform/networking.tf diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index e635519..e077368 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -75,3 +75,23 @@ resource "azurerm_storage_container" "defender_containers" { 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/networking.tf b/infrastructure/terraform/networking.tf new file mode 100644 index 0000000..1c64309 --- /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 4cf7965..bf2eeff 100644 --- a/infrastructure/terraform/output.tf +++ b/infrastructure/terraform/output.tf @@ -11,3 +11,11 @@ locals { 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 bab41c7..b7e1640 100644 --- a/infrastructure/terraform/variables.tf +++ b/infrastructure/terraform/variables.tf @@ -59,3 +59,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" +} From 313acc9a662347eed4be8b9e8ce439c359337e9a Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:37:00 +0100 Subject: [PATCH 4/6] DSTA-683: fixup container key logic --- infrastructure/terraform/defender.tf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index e077368..83a8b26 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -7,8 +7,10 @@ locals { ] defender_target_storage_accounts = { - for container_key, container in local.upload_container_urls : - container_key => container.storage_account_id + for storage_account_id in toset([ + for container in local.upload_container_urls : container.storage_account_id + ]) : + storage_account_id => storage_account_id } } From 4a31fa1d83ebf3173e2487ea30564ac77f0591cf Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:39:15 +0100 Subject: [PATCH 5/6] DSTA-683: fixup output object details Ensure hub environment is specified --- infrastructure/environments/dev/variables.sh | 1 + infrastructure/terraform/defender.tf | 2 +- infrastructure/terraform/output.tf | 7 ++++++- infrastructure/terraform/variables.tf | 4 ++++ 4 files changed, 12 insertions(+), 2 deletions(-) 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 83a8b26..4737574 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -8,7 +8,7 @@ locals { defender_target_storage_accounts = { for storage_account_id in toset([ - for container in local.upload_container_urls : container.storage_account_id + for container in local.upload_container_details : container.storage_account_id ]) : storage_account_id => storage_account_id } diff --git a/infrastructure/terraform/output.tf b/infrastructure/terraform/output.tf index bf2eeff..b63881f 100644 --- a/infrastructure/terraform/output.tf +++ b/infrastructure/terraform/output.tf @@ -1,11 +1,16 @@ locals { - upload_container_urls = { + upload_container_details = { for container_key, container in azurerm_storage_container.bso : 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" { diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf index b7e1640..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"{ From 4b52a83af841a6683b90f16ad779ffe0cc039c00 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:55:02 +0100 Subject: [PATCH 6/6] DSTA-683: format updates --- infrastructure/terraform/defender.tf | 7 ++----- infrastructure/terraform/main.tf | 6 +++--- infrastructure/terraform/networking.tf | 12 ++++++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/infrastructure/terraform/defender.tf b/infrastructure/terraform/defender.tf index 4737574..4f43379 100644 --- a/infrastructure/terraform/defender.tf +++ b/infrastructure/terraform/defender.tf @@ -1,5 +1,5 @@ locals { - defender_upload_storage_account_name =substr(replace("sa${var.app_short_name}${var.environment}defender", "/[^0-9a-z]/", ""), 0, 24) + defender_upload_storage_account_name = substr(replace("sa${var.app_short_name}${var.environment}defender", "/[^0-9a-z]/", ""), 0, 24) defender_containers = [ "clean-scans", @@ -7,10 +7,7 @@ locals { ] defender_target_storage_accounts = { - for storage_account_id in toset([ - for container in local.upload_container_details : container.storage_account_id - ]) : - storage_account_id => storage_account_id + (local.upload_storage_account_name) = azurerm_storage_account.upload.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 index 1c64309..991f149 100644 --- a/infrastructure/terraform/networking.tf +++ b/infrastructure/terraform/networking.tf @@ -1,9 +1,9 @@ -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" +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" {