learner
learner

Reputation: 1007

Using bicep to create a storage account that requires a private endpoint and a customer managed key

I am trying to use bicep to create an Azure storage account that uses a customer managed key. I have got something at the moment, but it is very messy, you have to run 3 x deployments and then a manual change to the storage account to link it with the key created within key vault. The last step involves running a PowerShell script that will rotate the key.

What am i trying to achieve ?

A simple modularised script that uses modules to achieve the above. The network configuration already exists and so does the key vault.

From what I have read up. It needs to be in the order below.

  1. Create a key within an existing key vault.
  2. Create a storage account that uses a private endpoint and link it to the key created in step 1
  3. Rotate the key using PowerShell if it exists.

I have got a template below which will almost achieve the above but without the private endpoint.

param keyName string = ''
param keyVersion string = ''
param vaultName string = ''
param location string = resourceGroup().location
param accountName string = 'tetsdfgfgdffd'

resource storageAcc 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  sku: {
    name: 'Standard_LRS'
    tier: 'Standard'
  }
  kind: 'Storage'
  name: accountName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    supportsHttpsTrafficOnly: true
  }
  dependsOn: []
}

resource keyVault 'Microsoft.KeyVault/vaults@2016-10-01' = {
  name: vaultName
  location: 'eastasia'
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    accessPolicies: []
    enabledForDeployment: true
    enabledForDiskEncryption: true
    enabledForTemplateDeployment: true
    enableSoftDelete: true
  }
}

module updateStorageAccount './enableEncryption.bicep' = {
  name: 'updateStorageAccount'
  params: {
    msiObjectId: storageAcc.identity.principalId
    vaultUri: keyVault.properties.vaultUri
    vaultName: vaultName
    accountName: accountName
    location: location
    keyName: keyName
    keyversion: keyVersion
  }
}

What have I got working so far ?

This creates the storage account.

var privateStorageBlobDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}'
var privateEndpointStorageBlobName = 'pep-${blobStorageAccountName}-blob-001'
var subnet = resourceId(vnetRG, 'Microsoft.Network/virtualNetworks/subnets', vnetName, subnetName)
var commonTags = {
  Application: applicationTag
  Criticality: criticalityTag
  Department: departmentTag
  Environment: environmentTag
  Owner: ownerTag
}
var appliedTags = union(commonTags, tags)


// Private Endpoints
resource privateEndpointStorageBlobName_blobPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-05-01' = {
  parent: privateEndpointStorageBlob
  name: 'blobPrivateDnsZoneGroup'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'config'
        properties: {
          privateDnsZoneId: extensionResourceId('/subscriptions/${dnsZoneSubscriptionId}/resourceGroups/${dnsZoneRG}', 'Microsoft.Network/privateDnsZones', privateStorageBlobDnsZoneName)
        }
      }
    ]
  }
}

resource privateEndpointStorageBlob 'Microsoft.Network/privateEndpoints@2021-05-01' = {
  name: privateEndpointStorageBlobName
  location: location
  properties: {
    subnet: {
      id: subnet
    }
    customNetworkInterfaceName: '${privateEndpointStorageBlobName}-nic'
    privateLinkServiceConnections: [
      {
        name: 'MyStorageBlobPrivateLinkConnection'
        properties: {
          privateLinkServiceId: blobStorageAccount.id
          groupIds: [
            'blob'
          ]
        }
      }
    ]
  }
  tags: appliedTags
}

resource blobStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: blobStorageAccountName
  location: location
  tags: appliedTags
  kind: 'StorageV2'
  sku: {
    name: skuName
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    accessTier: accessTier
    publicNetworkAccess: 'Disabled'
    allowBlobPublicAccess: false
    isHnsEnabled: isHnsEnabled
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
    }
  }
}


// Storage Account SystemAssigned Managed Identity
@description('The Principal ID of the System Assigned Managed Identity for the new Storage Account.')
output storageAccount_ManagedIdentity string = blobStorageAccount.identity.principalId

Attach the private end point to the created storage account.

var privateStorageBlobDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}'
var privateEndpointStorageBlobName = 'pep-${blobStorageAccountName}-blob-001'
var subnet = resourceId(vnetRG, 'Microsoft.Network/virtualNetworks/subnets', vnetName, subnetName)
var commonTags = {
  Application: applicationTag
  Criticality: criticalityTag
  Department: departmentTag
  Environment: environmentTag
  Owner: ownerTag
}
var appliedTags = union(commonTags, tags)


// Private Endpoints
resource privateEndpointStorageBlobName_blobPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-05-01' = {
  parent: privateEndpointStorageBlob
  name: 'blobPrivateDnsZoneGroup'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'config'
        properties: {
          privateDnsZoneId: extensionResourceId('/subscriptions/${dnsZoneSubscriptionId}/resourceGroups/${dnsZoneRG}', 'Microsoft.Network/privateDnsZones', privateStorageBlobDnsZoneName)
        }
      }
    ]
  }
}

resource privateEndpointStorageBlob 'Microsoft.Network/privateEndpoints@2021-05-01' = {
  name: privateEndpointStorageBlobName
  location: location
  properties: {
    subnet: {
      id: subnet
    }
    customNetworkInterfaceName: '${privateEndpointStorageBlobName}-nic'
    privateLinkServiceConnections: [
      {
        name: 'MyStorageBlobPrivateLinkConnection'
        properties: {
          privateLinkServiceId: blobStorageAccount.id
          groupIds: [
            'blob'
          ]
        }
      }
    ]
  }
  tags: appliedTags
}

resource blobStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: blobStorageAccountName
  location: location
  tags: appliedTags
  kind: 'StorageV2'
  sku: {
    name: skuName
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    accessTier: accessTier
    publicNetworkAccess: 'Disabled'
    allowBlobPublicAccess: false
    isHnsEnabled: isHnsEnabled
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
    }
  }
}


// Storage Account SystemAssigned Managed Identity
@description('The Principal ID of the System Assigned Managed Identity for the new Storage Account.')
output storageAccount_ManagedIdentity string = blobStorageAccount.identity.principalId

This creates the key within an existing vault.

param keyVaultname string
param keyName string
param keyOps array = [
  'sign'
  'verify'
  'encrypt'
  'decrypt'
  'wrapKey'
  'unwrapKey'
]
param keyType string = 'RSA'
param keySize int = 3072
param versionExpiryDays int = 180
param rotateBeforeExpiry int = 30

// Resource Lookups
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: keyVaultname
}


resource kvKey 'Microsoft.KeyVault/vaults/keys@2023-02-01' = {
  parent: keyVault
  name: keyName
  properties: {
    keyOps: keyOps
    attributes: {
      enabled: true
    }
    keySize: keySize
    kty: keyType

    rotationPolicy:{
      attributes: { 
        expiryTime: 'P${versionExpiryDays}D'
      }
      lifetimeActions: [
        {
          action: {
            type: 'rotate'
          }
          trigger: {
            timeBeforeExpiry: 'P${rotateBeforeExpiry}D'
          }
        }
      ]
    }
  }
}


// Outputs
output keyVersionUri string = kvKey.properties.keyUriWithVersion
output keyUri string = kvKey.properties.keyUri
output keyName string = kvKey.name

Then I have to manually link the key to the storage account configuration. The entire process is long winded and sub optimal. The last step is a PS script that I have to run to rotate the key.

Now rotate the key using Ps, ideally from the output of the bicep code such that its not a manual step

# Connect-AzAccount

$keyVaultsName = @('my-kv')
foreach ($KeyVaultName in $KeyVaultsName)
{
    $keys = Get-AzKeyVaultKey -VaultName $KeyVaultName
    foreach ($key in $keys) {
        # Rotate the key
        $newKeyVersion = Invoke-AzKeyVaultKeyRotation -VaultName $KeyVaultName -KeyName $key.Name

        Write-Host "Key rotation completed for $($key.Name). New version: $($newKeyVersion.Version)"
    }

    Write-Host "Key rotation completed for all keys in $KeyVaultName."
}

Upvotes: 0

Views: 2053

Answers (1)

Thomas
Thomas

Reputation: 29736

Few things:

  • Update your api-version to target newer api: this will give your more options
  • Use a user-assigned identity: you will be able to assign permission before creating the storage account
  • Achieve automatic rotation by auto-rotate in kv and not specifying the key version while deploying the storage account

Here is a complete bicep sample.

key-vault-role-assignment.bicep

param keyVaultName string
param principalId string
param principalType string = 'ServicePrincipal'
param roleId string

resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: keyVaultName
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().subscriptionId, resourceGroup().name, keyVaultName, roleId, principalId)
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId)
    principalId: principalId
    principalType: principalType
  }
}

main .bicep:

param location string = resourceGroup().location

param keyVaultName string = 'thomastestkv725'
param storageAccountName string = 'thomasteststr725'

param storageAccountCmkKeyName string = '${storageAccountName}-encryption-key'
param identityName string = '${storageAccountName}-identity'

// create a user assigned identity for the storage account
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: identityName
  location: location
}

// create a key vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: keyVaultName
  location: location
  properties: {
    enableRbacAuthorization: true
    enabledForTemplateDeployment: true
    enablePurgeProtection: true // required when enabling CMK
    enableSoftDelete: true // required when enabling CMK
    softDeleteRetentionInDays: 90
    publicNetworkAccess: 'Enabled' // require when enabling CMK
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices' // require when enabling CMK if firewall enable
    }
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
  }
}

// create the stroage account key
resource storageAccountCmkKey 'Microsoft.KeyVault/vaults/keys@2023-07-01' = {
  parent: keyVault
  name: storageAccountCmkKeyName
  properties: {
    kty: 'RSA' // CMK only supports RSA keys
    keySize: 4096
    attributes: {
      enabled: true
      exportable: false
    }
    rotationPolicy: {
      lifetimeActions: [
        {
          trigger: {
            timeAfterCreate: 'P5M' // 5 months
          }
          action: {
            type: 'rotate'
          }
        }
        {
          trigger: {
            timeBeforeExpiry: 'P1M' // 1 months
          }
          action: {
            type: 'notify'
          }
        }
      ]
      attributes: {
        expiryTime: 'P6M' // 6 months
      }
    }
  }
}

// grant kv crypo permission to the identity
module keyVaultRoleAssignment 'key-vault-role-assignment.bicep' = {
  name: '${identityName}-${keyVaultName}-rbac'
  params: {
    keyVaultName: keyVault.name
    principalId: identity.properties.principalId
    roleId: '12338af0-0e69-4776-bea7-57ae8d297424' // Key Vault Crypto User
  }
}

// Create the storage account once the role assignment is done
resource storageAcc 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  kind: 'StorageV2'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    supportsHttpsTrafficOnly: true
    encryption: {
      identity: {
        userAssignedIdentity: identity.id // use the user assigned identity
      }
      keySource: 'Microsoft.Keyvault'
      keyvaultproperties: {
        keyvaulturi: keyVault.properties.vaultUri
        keyname: storageAccountCmkKey.name
        keyversion: null // do not specify version for automatic rotation
      }
      services: {
        blob: {
          enabled: true
          keyType: 'Account'
        }
        file: {
          enabled: true
          keyType: 'Account'
        }
      }
    }
  }
  dependsOn: [ keyVaultRoleAssignment ]
}

Upvotes: 0

Related Questions