diff --git a/research-spoke/spoke-modules/airlock/adf.bicep b/research-spoke/spoke-modules/airlock/adf.bicep index 5b715b8..8d226d0 100644 --- a/research-spoke/spoke-modules/airlock/adf.bicep +++ b/research-spoke/spoke-modules/airlock/adf.bicep @@ -29,6 +29,9 @@ param pipelineNameFilesToFiles string = 'pipe-data_move-files_to_files' param debugMode bool +@description('The names of additional storage accounts (beyond the private storage account) that the Data Factory is allowed to access via its pipelines. Used to configure outbound access control.') +param allowedStorageAccountNames array = [] + import { pipelineNamesType } from '../../../shared-modules/types/pipelineNamesType.bicep' var baseName = !empty(subWorkloadName) @@ -41,6 +44,20 @@ var adlsGen2LinkedServiceName = 'ls_ADLSGen2_Generic' var azFilesLinkedServiceName = 'ls_AzFiles_Generic' var kvLinkedServiceName = 'ls_KeyVault' +// Outbound access control: compute the list of allowed domain names for the Azure Policy assignment +var storageSuffix = environment().suffixes.storage +var keyVaultSuffix = environment().suffixes.keyvaultDns +// Combine the private storage account with any additional allowed storage accounts +var allStorageAccountNames = union([privateStorageAcctName], allowedStorageAccountNames) +// Generate blob, dfs, and file endpoints for each storage account +var storageAllowedDomains = flatten(map(allStorageAccountNames, name => [ + '${name}.blob.${storageSuffix}' + '${name}.dfs.${storageSuffix}' + '${name}.file.${storageSuffix}' +])) +// Include the Key Vault used by the Key Vault linked service to retrieve connection strings +var allowedDomains = concat(storageAllowedDomains, ['${keyVaultName}${keyVaultSuffix}']) + resource privateStorageAcct 'Microsoft.Storage/storageAccounts@2021-08-01' existing = { name: privateStorageAcctName } @@ -511,6 +528,25 @@ resource adfPrivateStgRole 'Microsoft.Authorization/roleAssignments@2022-04-01' } } +// Assign the built-in Azure Policy to restrict ADF outbound access to allowed domains only. +// This prevents data exfiltration by blocking pipeline communication with unapproved destinations. +// Policy: "[Preview]: Azure Data Factory pipelines should only communicate with allowed domains" +resource adfOutboundPolicyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = { + name: take(replace(baseName, '{rtype}', 'pa-adf-out'), 64) + scope: resourceGroup() + properties: { + policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/3d02a511-74e5-4dab-a5fd-878704d4a61a' + parameters: { + effect: { + value: 'Deny' + } + allowedDomainNames: { + value: allowedDomains + } + } + } +} + output principalId string = adf.identity.principalId output name string = adf.name diff --git a/research-spoke/spoke-modules/airlock/main.bicep b/research-spoke/spoke-modules/airlock/main.bicep index ead0291..b0c1fbe 100644 --- a/research-spoke/spoke-modules/airlock/main.bicep +++ b/research-spoke/spoke-modules/airlock/main.bicep @@ -250,12 +250,26 @@ module adfModule 'adf.bicep' = { usePrivateEndpoint: usePrivateEndpoints privateEndpointSubnetId: usePrivateEndpoints ? privateEndpointSubnetId : '' privateDnsZonesResourceGroupId: usePrivateEndpoints ? privateDnsZonesResourceGroupId : '' + + // Storage accounts the ADF is allowed to access (for outbound access control policy) + allowedStorageAccountNames: [ + publicStorageAccountNameModule.outputs.validName + airlockStorageAccountNameForPolicy + ] } } var airlockStorageAccountName = useCentralizedReview ? centralizedModule.outputs.centralAirlockStorageAccountName : spokeAirlockStorageAccountModule.outputs.storageAccountName + +// Derive the airlock storage account name without taking a dependency on centralizedModule +// (which itself depends on adfModule, which would create a circular dependency). +// For centralized review, extract the name from the resource ID parameter. +// For non-centralized review, use the spoke airlock storage account module output. +var airlockStorageAccountNameForPolicy = useCentralizedReview + ? last(split(centralAirlockResources.storageAccountId, '/')) + : spokeAirlockStorageAccountModule.outputs.storageAccountName var airlocKeyVaultUri = useCentralizedReview ? centralizedModule.outputs.centralKeyVaultUri : keyVault.properties.vaultUri