How to capture secrets and configurations using Azure Bicep

A simple way to capture secrets and configurations for every Azure Bicep resource and store them in Azure Key Vault

Today, we will learn how to capture the secrets, configurations or both from resources created using Azure Bicep. This enables the complete automation of infrastructure deployment, eliminating the need for human intervention. Instead of manually traversing individual resources to retrieve valuable information like connection strings, keys, or other configurations, we can streamline the process.

I have published on Github a repository that contains the code from this post.

├── 📁 .github
│   ├── 📁 workflows
│   │   ├── 📄 deploy.yml
├── 📁 src
│   ├── 📁 modules
│   │   ├── 📄 key-vault-secret.bicep (*)
│   │   ├── 📄 key-vault.bicep
│   │   ├── 📄 static-web-app.bicep
│   │   ├── 📄 storage-account.bicep
│   ├── 📄 template.bicep
└── 📄 .gitignore

We need to create several Azure Bicep modules which are individual files containing configurations and resources that can be used by others Azure Bicep files.

Explaining key-vault.bicep file

We need a key-vault.bicep file, which we will use to create an Azure Key Vault resource.

Two important points:

  • You need to create at least one access policy, adding the objectId of the service principal you will use to initiate the process. This is necessary because that Service Principal requires specific permissions in Key Vault to create secrets.
  • If you want, you can also add your own user identifier in order to be able to personally query the secrets. Do it by hardcoding the identifiers in the template or passing parameters. If you manually create the access policy for yourself, the next time you run the Azure Bicep template, it will disappear.
  • It’s necessary to extract the name of the Azure Key Vault account with output name string = keyVault.name because it will be used later in other modules.
@description('key vault name')
param name string

@description('object id of service principal that creates resources on azure')
@minLength(36)
@maxLength(36)
param objectId string

@description('tenant id')
@minLength(36)
@maxLength(36)
param tenantId string

// resource

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
  name: name
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'standard'
      family: 'A'
    }
    softDeleteRetentionInDays: 7
    enableSoftDelete: true
    tenantId: tenantId
    accessPolicies: [
      {
        objectId: objectId
        tenantId: tenantId
        applicationId: ''
        permissions: {
          keys: ['all']
          secrets: ['all']
          certificates: ['all']
          storage: ['all']
        }
      }
    ]
  }
}

// outputs 

output name string = keyVault.name

Explaining key-vault-secret.bicep file

The following module key-vault-secret.bicep contains input parameters such as name which will be used to reference an existing Azure Key Vault account, and a combination of secretName and secretValue. Please note that secretName is surrounded by toUpper() method, as our team has agreed upon this convention.

Essentially, the module will reference an existing Azure Key Vault account using the existing keyword and then create a secret within it.

@description('key vault name')
param name string

@description('secret name')
param secretName string

@secure()
@description('secret value')
param secretValue string

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: name
}

resource secret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
  parent: keyVault
  name: toUpper(secretName)
  properties: {
    value: secretValue
    contentType: 'plain/text'
  }
}

Now, this module can be used in other templates or modules whenever you need to store a secret or configuration.

Explaining static-web-app.bicep file: how to capture deployment token and hostname

We created a module named static-web-app.bicep to create an Static Web App with basic configuration.

Pay attention to the final part of the file.

@description('static web app name')
param name string

@description('key vault name')
param keyVaultName string

@description('deployment random key')
param randomKey string

@description('static web app tier')
@allowed(['Free', 'Standard'])
param skuTier string = 'Free'

@description('static web app tier')
@allowed(['Free', 'Standard'])
param skuName string = 'Free'

// resource

resource staticWebApp 'Microsoft.Web/staticSites@2021-03-01' = {
  name: name
  location: resourceGroup().location
  sku: {
    name: skuName   
    tier: skuTier
  }
  properties: { }
}

// outputs to key vault 

module secret1 'key-vault-secret.bicep' = {
  name: 'static-app-${name}-deployment-token-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'DEPLOYMENT-TOKEN-${toUpper(name)}'
    secretValue: staticWebApp.listSecrets().properties.apiKey
  }
}

module secret2 'key-vault-secret.bicep' = {
  name: 'static-app-${name}-hostname-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'HOSTNAME-${toUpper(name)}'
    secretValue: staticWebApp.properties.defaultHostname
  }
}

As you can see, by calling the key-vault-secret.bicep module from static-web-app.bicep we can create a deployment that generates a secret within our Azure Key Vault.

module secret1 'key-vault-secret.bicep' = {
  name: 'static-app-${name}-deployment-token-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'DEPLOYMENT-TOKEN-${toUpper(name)}'
    secretValue: staticWebApp.listSecrets().properties.apiKey
  }
}

module secret2 'key-vault-secret.bicep' = {
  name: 'static-app-${name}-hostname-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'HOSTNAME-${toUpper(name)}'
    secretValue: staticWebApp.properties.defaultHostname
  }
}

The secretValue property could change for each resource because the nature of the secret could be different an so on, the way to access to it

The way we populate the secretValue property can vary depending on the type of resource, of course. In some cases, we will use listSecrets(), listKeys(), listCredentials() methods. Make sure to use the correct mechanism to obtain the value you desire.

To include the static-web-app.bicep module, you just need a snippet like this in your main Azure Bicep file.

module staticApp './modules/static-web-app.bicep' = {
   name: 'static-web-app-${randomKey}'
   scope: resourceGroup
   params: {
    name: 'my-static-web-app-${suffix}'
    keyVaultName: keyVault.outputs.name
    randomKey: randomKey
    skuName: 'Free'
    skuTier: 'Free'
   }
   dependsOn: [ keyVault ]
}

Note that this is is just a portion of the main template.bicep file

Explaining storage-account.bicep file: how to capture primary and secondary connection string

In this case, we can see the storage-account.bicep file and how to create an Azure Storage account and how to retrieve the primary and secondary connection strings.

@minLength(3)
@maxLength(24)
@description('storage account name')
param name string

@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_RAGRS', 'Standard_ZRS', 'Premium_LRS'])
param skuName string = 'Standard_LRS'

@allowed(['Standard', 'Premium'])
param skuTier string = 'Standard'

@allowed(['Storage', 'StorageV2', 'BlobStorage', 'FileStorage', 'BlockBlobStorage'])
param kind string = 'StorageV2'

@description('key vault name')
param keyVaultName string

@description('deployment random key')
param randomKey string

// resource

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: name
  location: resourceGroup().location
  sku: {
    name: skuName
    tier: skuTier
  }
  kind: kind
  properties: {
    publicNetworkAccess: 'Enabled'
  }
}

// outputs

var storageAccountKeys = storageAccount.listKeys()

// outputs to key vault

module secret1 './key-vault-secret.bicep' = {
  name: 'stg-account-${name}-pri-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'STORAGE-ACCOUNT-${toUpper(name)}-PRIMARY-CONNECTION-STRING'
    secretValue: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccountKeys.keys[0].value};EndpointSuffix=core.windows.net'    
  }
}

module secret2 './key-vault-secret.bicep' = {
  name: 'stg-account-${name}-sec-${randomKey}'
  params: {
    name: keyVaultName
    secretName: 'STORAGE-ACCOUNT-${toUpper(name)}-SECONDARY-CONNECTION-STRING'
    secretValue: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccountKeys.keys[1].value};EndpointSuffix=core.windows.net'    
  }
}

To include the storage-account.bicep module, you just need a snippet like this in your main Azure Bicep file.

module storageAccount './modules/storage-account.bicep' = {
  name: 'storage-account-${randomKey}'
  scope: resourceGroup
  params: {
    name: 'mystorageaccount${suffix}'
    kind: 'StorageV2'
    skuName: 'Standard_LRS'
    skuTier:  'Standard'
    keyVaultName: keyVault.outputs.name
    randomKey: randomKey
  }
  dependsOn: [ keyVault ]
}

Explaining template.bicep file

Finally, our main template.bicep file is responsible for calling each module that we need one by one.

There are several important points you should be aware of to understand how this file works:

  • We have a parameter named randomKey that we feed from our GitHub Action with the value github.run_id so that each deployment has a unique identifier, regardless of how many times we launch it.
  • We use the parameters objectId and tenantId when creating the Azure Key Vault resource to establish access policies. You can find the objectId parameter from Enterprise Applications blade
  • We use a constant named suffix with a semi-random value to ensure that the names of the resources we are creating do not collide with existing ones.
targetScope = 'subscription'

@description('The location of the resource group')
param location string

@description('a random key to ensure deployments are unique')
param randomKey string = newGuid()

@description('service principal object id')
param objectId string

@description('tenant id')
param tenantId string

var suffix = '77697'

resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: 'my-resource-group-${suffix}'
  location: location
}

module keyVault './modules/key-vault.bicep' = {
  name: 'key-vault-${randomKey}'
  scope: resourceGroup
  params: {
    name: 'my-key-vault-${suffix}'
    objectId: objectId  
    tenantId: tenantId
  }
}

module staticApp './modules/static-web-app.bicep' = {
   name: 'static-web-app-${randomKey}'
   scope: resourceGroup
   params: {
    name: 'my-static-web-app-${suffix}'
    keyVaultName: keyVault.outputs.name
    randomKey: randomKey
    skuName: 'Free'
    skuTier: 'Free'
   }
   dependsOn: [ keyVault ]
}

module storageAccount './modules/storage-account.bicep' = {
  name: 'storage-account-${randomKey}'
  scope: resourceGroup
  params: {
    name: 'mystorageaccount${suffix}'
    kind: 'StorageV2'
    skuName: 'Standard_LRS'
    skuTier:  'Standard'
    keyVaultName: keyVault.outputs.name
    randomKey: randomKey
  }
  dependsOn: [ keyVault ]
}

Does this works?

Yes, we can run the GitHub Action file

After navigating to the Azure Portal, we can find a resource group named my-resource-group-77697, along with some resources within it.

Furthermore, by accessing the Azure Key Vault resource, we can locate the expected secrets

And… that’s it folks!