Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions research-spoke/spoke-modules/airlock/adf.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions research-spoke/spoke-modules/airlock/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down