Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
948b7f6
Update to latest
AlexanderSehr Nov 13, 2025
c8d68e2
Update to latest
AlexanderSehr Nov 13, 2025
be3cd81
Update to latest
AlexanderSehr Nov 13, 2025
2afd656
Update to latest
AlexanderSehr Nov 13, 2025
cbe5ab8
Update to latest
AlexanderSehr Nov 13, 2025
edc6e71
Update to latest
AlexanderSehr Nov 13, 2025
cf5bb9b
Update to latest
AlexanderSehr Nov 13, 2025
92fd665
Fixed role assignment
AlexanderSehr Nov 13, 2025
11968d3
Update to latest
AlexanderSehr Nov 13, 2025
b3af4b9
Update to latest
AlexanderSehr Nov 13, 2025
cbdc626
Updated sys assigned
AlexanderSehr Nov 13, 2025
12600b6
Update to latest
AlexanderSehr Nov 13, 2025
302f391
Merge branch 'main' into users/alsehr/synapseHSM
AlexanderSehr Nov 15, 2025
d95ef80
Re-added role assignment + aligned identity format
AlexanderSehr Nov 15, 2025
6b4052e
Update to latest
AlexanderSehr Nov 15, 2025
4cad98c
Update to latest
AlexanderSehr Nov 15, 2025
5814279
Update to latest
AlexanderSehr Nov 15, 2025
b8c566d
Update to latest
AlexanderSehr Nov 15, 2025
562cc99
Update to latest
AlexanderSehr Nov 15, 2025
731d8c9
Update to latest
AlexanderSehr Nov 15, 2025
f28bc9a
Update to latest
AlexanderSehr Nov 15, 2025
68fe9f4
Update to latest
AlexanderSehr Nov 15, 2025
8ba63f1
Update to latest
AlexanderSehr Nov 15, 2025
2d7f065
Update to latest
AlexanderSehr Nov 15, 2025
4b03c9b
Update to latest
AlexanderSehr Nov 15, 2025
6772bc1
Test with version for hsm
AlexanderSehr Nov 17, 2025
3f7e69b
Update to latest
AlexanderSehr Nov 17, 2025
16da3a0
Update to latest
AlexanderSehr Nov 17, 2025
0e41a38
Changed to crypto user for testing
AlexanderSehr Nov 18, 2025
d40362d
Update to latest
AlexanderSehr Nov 18, 2025
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
5 changes: 4 additions & 1 deletion .github/workflows/avm.res.synapse.workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ jobs:
uses: ./.github/workflows/avm.template.module.yml
with:
workflowInput: "${{ needs.job_initialize_pipeline.outputs.workflowInput }}"
moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}"
# moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}"
# moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-hsm-uami","path":"tests/e2e/cmk-hsm-uami/main.test.bicep"}, {"e2eIgnore":false,"name":"cmk-sami","path":"tests/e2e/cmk-sami/main.test.bicep"}]'
# moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-sami","path":"tests/e2e/cmk-sami/main.test.bicep"}]' works
moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-hsm-uami","path":"tests/e2e/cmk-hsm-uami/main.test.bicep"}]'
Comment on lines +86 to +89
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}"
# moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-hsm-uami","path":"tests/e2e/cmk-hsm-uami/main.test.bicep"}, {"e2eIgnore":false,"name":"cmk-sami","path":"tests/e2e/cmk-sami/main.test.bicep"}]'
# moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-sami","path":"tests/e2e/cmk-sami/main.test.bicep"}]' works
moduleTestFilePaths: '[{"e2eIgnore":false,"name":"cmk-hsm-uami","path":"tests/e2e/cmk-hsm-uami/main.test.bicep"}]'
moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}"

psRuleModuleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.psRuleModuleTestFilePaths }}"
modulePath: "${{ needs.job_initialize_pipeline.outputs.modulePath}}"
secrets: inherit
11 changes: 7 additions & 4 deletions avm/res/synapse/workspace/key/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ param isActiveCMK bool
@description('Required. The resource ID of a key vault to reference a customer managed key for encryption from.')
param keyVaultResourceId string

resource cMKKeyVault 'Microsoft.KeyVault/vaults@2024-11-01' existing = {
name: last(split(keyVaultResourceId, '/'))
var isHSMManagedCMK = split(keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!isHSMManagedCMK) {
name: last(split((keyVaultResourceId), '/'))
scope: resourceGroup(split(keyVaultResourceId, '/')[2], split(keyVaultResourceId, '/')[4])

resource cMKKey 'keys@2024-11-01' existing = {
resource cMKKey 'keys@2025-05-01' existing = if (!isHSMManagedCMK) {
name: name
}
}
Expand All @@ -31,7 +32,9 @@ resource key 'Microsoft.Synapse/workspaces/keys@2021-06-01' = {
parent: workspace
properties: {
isActiveCMK: isActiveCMK
keyVaultUrl: cMKKeyVault::cMKKey.properties.keyUri
keyVaultUrl: !isHSMManagedCMK
? cMKKeyVault::cMKKey!.properties.keyUri
: 'https://${last(split((keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${name}'
}
}

Expand Down
72 changes: 37 additions & 35 deletions avm/res/synapse/workspace/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ param defaultDataLakeStorageCreateManagedPrivateEndpoint bool = false
@description('Optional. The Entra ID administrator for the synapse workspace.')
param administrator administratorType?

import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType?

@description('Optional. Assign permissions for the customer managed key to the workspace\'s system-assigned identity. Only supports key stored in Azure Key Vaults.')
param customerManagedKeyGrantSysAssignedAccess bool = true

@description('Optional. Activate workspace by adding the system managed identity in the KeyVault containing the customer managed key and activating the workspace.')
param encryptionActivateWorkspace bool = false

Expand Down Expand Up @@ -91,46 +94,47 @@ param accountUrl string = 'https://${last(split(defaultDataLakeStorageAccountRes
@description('Optional. Git integration settings.')
param workspaceRepositoryConfiguration object?

import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@description('Optional. The managed identity definition for this resource.')
param managedIdentities managedIdentityOnlyUserAssignedType?

import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.6.0'
@description('Optional. The lock settings of the service.')
param lock lockType?

import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?

import { privateEndpointMultiServiceType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
param privateEndpoints privateEndpointMultiServiceType[]?

import { diagnosticSettingLogsOnlyType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
import { diagnosticSettingLogsOnlyType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@description('Optional. The diagnostic settings of the service.')
param diagnosticSettings diagnosticSettingLogsOnlyType[]?

// Variables

var enableReferencedModulesTelemetry = false

var cmkUserAssignedIdentityAsArray = !empty(customerManagedKey.?userAssignedIdentityResourceId ?? [])
? [customerManagedKey.?userAssignedIdentityResourceId]
: []

var userAssignedIdentitiesUnion = !empty(managedIdentities)
? union(managedIdentities.?userAssignedResourceIds ?? [], cmkUserAssignedIdentityAsArray)
: cmkUserAssignedIdentityAsArray

var formattedUserAssignedIdentities = reduce(
map((userAssignedIdentitiesUnion ?? []), (id) => { '${id}': {} }),
map(
union(
(managedIdentities.?userAssignedResourceIds ?? []),
(!empty(customerManagedKey.?userAssignedIdentityResourceId)
? [customerManagedKey.?userAssignedIdentityResourceId]
: [])
),
(id) => { '${id}': {} }
),
{},
(cur, next) => union(cur, next)
) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }

// The resource must always be deployed with at least a system-assigned identity
var identity = {
type: !empty(userAssignedIdentitiesUnion) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned'
type: !empty(formattedUserAssignedIdentities) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned'
userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
}

Expand Down Expand Up @@ -182,15 +186,16 @@ resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableT
}
}

resource cMKKeyVault 'Microsoft.KeyVault/vaults@2024-11-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId)) {
name: last(split((customerManagedKey.?keyVaultResourceId!), '/'))
var isHSMManagedCMK = split(customerManagedKey.?keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
name: last(split((customerManagedKey!.keyVaultResourceId), '/'))
scope: resourceGroup(
split(customerManagedKey.?keyVaultResourceId!, '/')[2],
split(customerManagedKey.?keyVaultResourceId!, '/')[4]
split(customerManagedKey!.keyVaultResourceId, '/')[2],
split(customerManagedKey!.keyVaultResourceId, '/')[4]
)

resource cMKKey 'keys@2024-11-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId) && !empty(customerManagedKey.?keyName)) {
name: customerManagedKey.?keyName!
resource cMKKey 'keys@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
name: customerManagedKey!.keyName
}
}

Expand Down Expand Up @@ -231,7 +236,9 @@ resource workspace 'Microsoft.Synapse/workspaces@2021-06-01' = {
useSystemAssignedIdentity: empty(customerManagedKey.?userAssignedIdentityResourceId)
}
key: {
keyVaultUrl: cMKKeyVault::cMKKey!.properties.keyUri
keyVaultUrl: !isHSMManagedCMK
? cMKKeyVault::cMKKey!.properties.keyUri
: 'https://${last(split((customerManagedKey!.?keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}'
name: customerManagedKey!.keyName
}
}
Expand Down Expand Up @@ -272,29 +279,24 @@ module synapse_integrationRuntimes 'integration-runtime/main.bicep' = [
]

// Workspace encryption with customer managed keys
// - Assign Synapse Workspace MSI access to encryption key
module workspace_cmk_rbac 'modules/nested_cmkRbac.bicep' = if (encryptionActivateWorkspace) {
// - Assign Synapse Workspace's (mandatory) system-assigned identity access to encryption key
module workspace_cmk_rbac 'modules/nested_cmkRbac.bicep' = if (customerManagedKeyGrantSysAssignedAccess && !empty(customerManagedKey) && !isHSMManagedCMK) {
name: '${workspace.name}-cmk-rbac'
params: {
workspaceIndentityPrincipalId: workspace.identity.principalId
keyvaultName: !empty(customerManagedKey!.keyVaultResourceId) ? cMKKeyVault.name : ''
usesRbacAuthorization: !empty(customerManagedKey!.keyVaultResourceId)
? cMKKeyVault!.properties.enableRbacAuthorization
: true
usesRbacAuthorization: cMKKeyVault!.properties.enableRbacAuthorization
keyName: customerManagedKey!.keyName
keyVaultName: last(split(customerManagedKey!.keyVaultResourceId, '/'))
}
scope: resourceGroup(
split(customerManagedKey.?keyVaultResourceId!, '/')[2],
split(customerManagedKey.?keyVaultResourceId!, '/')[4]
)
}

// - Workspace encryption - Activate Workspace
module workspace_key 'key/main.bicep' = if (encryptionActivateWorkspace) {
module workspace_key 'key/main.bicep' = if (encryptionActivateWorkspace && !empty(customerManagedKey)) {
name: take('${workspace.name}-cmk-activation', 64)
params: {
keyVaultResourceId: customerManagedKey!.keyVaultResourceId
name: customerManagedKey!.keyName
isActiveCMK: true
keyVaultResourceId: cMKKeyVault.id
workspaceName: workspace.name
}
dependsOn: [
Expand Down Expand Up @@ -410,7 +412,7 @@ module workspace_sqlPools 'sql-pool/main.bicep' = [
]

// Endpoints
module workspace_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [
module workspace_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.1' = [
for (privateEndpoint, index) in (privateEndpoints ?? []): {
name: '${uniqueString(deployment().name, location)}-workspace-PrivateEndpoint-${index}'
scope: resourceGroup(
Expand Down Expand Up @@ -579,7 +581,7 @@ type firewallRuleType = {
}

import { autoScaleType, dynamicExecutorAllocationType, sparkConfigPropertiesType } from 'big-data-pool/main.bicep'
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.6.1'
@export()
@description('The synapse workspace Big Data Pool definition.')
type bigDataPoolType = {
Expand Down
24 changes: 18 additions & 6 deletions avm/res/synapse/workspace/modules/nested_cmkRbac.bicep
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
param keyvaultName string
@description('Required. The name of the key vault to assign permissions in/to.')
param keyVaultName string

@description('Required. The principal ID of the Synapse Workspace System Identity to assign permissions to.')
param workspaceIndentityPrincipalId string

@description('Required. Whether or not the referenced Key Vault uses RBAC authorization model.')
param usesRbacAuthorization bool = false

@description('Required. Name of the key to set the permissions for.')
param keyName string

// Workspace encryption - Assign Workspace System Identity Keyvault Crypto Reader at Encryption Keyvault
resource keyVault 'Microsoft.KeyVault/vaults@2024-11-01' existing = {
name: keyvaultName
name: keyVaultName

resource key 'keys@2025-05-01' existing = {
name: keyName
}
}

// Assign RBAC role Key Vault Crypto User
resource workspace_cmk_rbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (usesRbacAuthorization) {
name: guid('${keyVault.id}-${workspaceIndentityPrincipalId}-Key-Vault-Crypto-User')
name: guid('${keyVault.id}-${workspaceIndentityPrincipalId}-Key-Vault-Crypto-Service-Encryption-User')
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'12338af0-0e69-4776-bea7-57ae8d297424'
)
'e147488a-f6f5-4113-8e2d-b22465e65bf6'
) // Key Vault Crypto Service Encryption User
principalId: workspaceIndentityPrincipalId
principalType: 'ServicePrincipal'
}
scope: keyVault
scope: keyVault::key
}

// Assign Access Policy for Keys
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The test is skipped because running the HSM scenario requires a persistent Managed HSM instance to be available and configured at all times, which would incur significant costs for contributors.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
echo "Checking role assignment for HSM: $1, Key: $2, Principal: $3"
# Allow key reference via identity
result=$(az keyvault role assignment list --hsm-name "$1" --scope "/keys/$2" --query "[?principalId == \`$3\` && roleName == \`Managed HSM Crypto Service Encryption User\`]")

if [[ "$result" != "[]" ]]; then
echo "Role assignment already exists."
else
echo "Role assignment not yet existing. Creating."
az keyvault role assignment create --hsm-name "$1" --role "Managed HSM Crypto Service Encryption User" --scope "/keys/$2" --assignee $3
fi

# Allow usage via ARM
az keyvault setting update --hsm-name $1 --name 'AllowKeyManagementOperationsThroughARM' --value 'true'
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@description('Optional. The location to deploy to.')
param location string = resourceGroup().location

@description('Required. The name of the Storage Account to create.')
param storageAccountName string

@description('Required. The name of the Managed Identity to create.')
param managedIdentityName string

@description('Required. The name of the Deployment Script to configure the HSM Key permissions.')
param deploymentScriptName string

@description('Required. The resource ID of the Managed Identity used by the deployment script. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-deploymentMSIName\'.')
@secure()
param deploymentMSIResourceId string

@description('Required. The resource ID of the managed HSM used for encryption. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-managedHSMResourceId\'.')
@secure()
param managedHSMResourceId string

@description('Required. The name of the HSMKey Vault Encryption Key.')
param keyName string

resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
isHnsEnabled: true
}

resource blobService 'blobServices@2025-01-01' = {
name: 'default'

resource container 'containers@2025-01-01' = {
name: 'synapsews'
}
}
}

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
name: managedIdentityName
location: location
}

module allowHsmAccess 'br/public:avm/res/resources/deployment-script:0.5.2' = {
name: '${uniqueString(deployment().name, location)}-hmsKeyPermissions'
params: {
name: deploymentScriptName
kind: 'AzureCLI'
azCliVersion: '2.67.0'
arguments: '"${last(split(managedHSMResourceId, '/'))}" "${keyName}" "${managedIdentity.properties.principalId}"'
scriptContent: loadTextContent('Set-mHMSKeyConfig.sh')
retentionInterval: 'P1D'
managedIdentities: {
userAssignedResourceIds: [
deploymentMSIResourceId
]
}
}
}

@description('The resource ID of the created Storage Account.')
output storageAccountResourceId string = storageAccount.id

@description('The name of the created container.')
output storageContainerName string = storageAccount::blobService::container.name

@description('The principal ID of the created Managed Identity.')
output managedIdentityPrincipalId string = managedIdentity.properties.principalId

@description('The name of the created Managed Identity.')
output managedIdentityName string = managedIdentity.name

@description('The resource ID of the created Managed Identity.')
output managedIdentityResourceId string = managedIdentity.id
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@description('Required. The name of the key to create in the HSM.')
param hsmKeyName string

@description('Required. The name of the managed HSM used for encryption.')
@secure()
param managedHSMName string

resource managedHsm 'Microsoft.KeyVault/managedHSMs@2025-05-01' existing = {
name: managedHSMName

resource key 'keys@2025-05-01' existing = {
name: hsmKeyName
}
// resource key 'keys@2025-05-01' = {
// name: hsmKeyName
// properties: {
// keySize: 3072 // Not supporting 4096
// kty: 'RSA-HSM'
// }
// }
}

@description('The resource ID of the HSM Key Vault.')
output keyVaultResourceId string = managedHsm.id

@description('The name of the HSMKey Vault Encryption Key.')
output keyName string = managedHsm::key.name

@description('The version of the HSMKey Vault Encryption Key.')
output keyVersion string = last(split(managedHsm::key.properties.keyUriWithVersion, '/'))
Loading
Loading