Using Key Vault Secrets As Secure Parameters In Azure Bicep - Template & Module Inputs

Many cloud resources expect secret values to be passed as part of their definition or configuration to function correctly. This could be passwords, keys, connection strings, certificates, etc. Keeping these values secure is necessary to prevent many sorts of security problems which easily arise if our secrets are not safe.

In Azure, using Key Vault is the preferred way of storing and managing secrets, certificates, and keys. When working with Azure Bicep, we often need to retrieve secrets stored in a key vault to later pass them into the definition of some resource.

In parameter files, key vault secret is referenced by specifying key vault resource id, secretName and (optionally) secretVersion. When working with modules, Azure Bicep getSecret function should be used to pass secrets into the module (nested deployment).

Fortunately, ARM templates and Azure Bicep have built-in support for using key vault secrets inside of the templates. In this post, we will discuss how to work with secure parameters and key vault secrets in Azure Bicep.

Contents:

Overview

We start with a short introduction about defining secure parameters in Azure Bicep files followed by a sample template which expects a secret value, this template will be used in code samples throughout this post.

Next, our attention shifts to key vault secrets. The first use case is how to pass a secret stored in a key vault into a Bicep file using a parameter file. The second one is how to pass a key vault secret when consuming a module in a template. Additionally, we briefly cover how to specify a particular version of a secret, not only the latest one.

The last part of the post covers a more advanced use case where we want to pass many key vault secrets into a Bicep template in form of an array. This avoids defining individual secure parameters for each key vault secret and instead using array and loops to handle data.

Parameter With @secure() Decorator

By default, parameters that we pass are not protected, their values are logged during the deployment and saved to the deployment history. This is convenient and works fine in many cases, but sometimes we want values to be kept secret.

Often, templates require some secret values to be provided, for example, passwords. In this case, we shouldn’t store secrets in template in plain text, but should rather pass them as parameters. For this kind of parameters ARM templates have secureString and secureObject data types.

In Bicep, there are no secureString and secureObject data types, instead there’s a @secure() decorator which can be applied to a parameter with a data type string or object, see the example below.

@secure()
param myPassword string

@secure()
param myConfig object

When transpiled into ARM template, the parameters above result into secureString and secureObject data types.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "myPassword": {
      "type": "secureString"
    },
    "myConfig": {
      "type": "secureObject"
    }
  },
  "resources": []
}

Sample Template With Secure Parameter

A real world example can be a deployment of a SQL database where we pass admin password as a secure parameter. The filename of template below is sqldb.bicep file and we’ll use this template in the next sections in our code examples.

// ============ sqldb.bicep ============

@secure()
param myPassword string

resource sqlserver 'Microsoft.Sql/servers@2021-02-01-preview' = {
  name: 'ContosoSqlServer'
  location: 'westus'
  properties: {
    administratorLogin: 'contosoadmin'
    administratorLoginPassword: myPassword
  }

  resource sqldb 'databases' = {
    name: 'contosodb'
    location: 'westus'
  }
}

Key Vault Secret Through Parameter File

In Bicep, we can use a parameter file to pass values, as we do it with ARM templates. And in a parameter file, we can reference secrets from a key vault by specifying the name of the vault and the name of the secret. Optionally, secret version can be also included, but by default the latest version is taken.

NOTE: Key Vault must be enabled for template deployment so that ARM has permissions to retrieve secrets during deployment. This can be enabled in Azure Portal under “Access policies” → “Azure Resource Manager for template deployment”, or enabledForTemplateDeployment property in an ARM template of a Key Vault.

Passing Secret

Consider passing myPassword parameter to the sqldb.bicep template from the last section. Our parameter file will look the one shown below. This code will fetch the latest version of the secret because it is the default behavior.

Please note that a few basic things are assumed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "myPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/1a286532-7724-438b-97a0-68278053737a/resourceGroups/rg-contoso/providers/Microsoft.KeyVault/vaults/kv-contoso"
        },
        "secretName": "mySqlPassword"
      }
    }
  }
}

Passing Specific Secret Version

Building on the previous example, if we want to take a specific version of the secret, then we should use secretVersion property where we pass a particular secret version. Obviously, the secret version should be a valid one, otherwise the deployment will fail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "myPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/1a286532-7724-438b-97a0-68278053737a/resourceGroups/rg-contoso/providers/Microsoft.KeyVault/vaults/kv-contoso"
        },
        "secretName": "mySqlPassword",
        "secretVersion": "2cc1676124b77bc9a1bfd30d8f4b6225"
      }
    }
  }
}

Deploying Template & Parameter File

There are multiple ways to deploy a bicep file with its parameter file. Here an example how to do that using Azure PowerShell. Read more about deploying Azure Bicep files in another post 5 Ways To Deploy Bicep File With Parameters - Azure DevOps, PowerShell, CLI, Portal, Cloud Shell.

New-AzResourceGroupDeployment `
  -Name mySqlDatabaseDeployment1 `
  -ResourceGroupName rg-contoso `
  -TemplateFile sqldb.bicep `
  -TemplateParameterFile sqldb.parameters.json

Passing Key Vault Secret into Module

In the last section, we explored how to pass key vault secrets into a template through secure parameters. But how to do this with modules? Assuming we have a Bicep module which expects some secret value, we want to pass a key vault secret there.

To learn more about modules, read Learn Modules In Azure Bicep post.

The idea is quite similar to the one discussed in the previous section, in a regular ARM template we’d have a nested deployment with a secure parameter, and the parameter value would be referenced from a key vault.

For Azure Bicep, there is a getSecret function which makes simplifies passing key vault secrets into modules. Let’s take a look at it next.

getSecret function

The getSecret function is specifically designed to be used in situations where a key vault secret needs to be passed as a secure parameter into a module. A few notes regarding getSecret function:

getSecret(secretName: string, [secretVersion: string])
// Usage: kv.getSecret('mySqlPassword', '2cc1676124b77bc9a1bfd30d8f4b6225')

Template To Deploy

Now, let’s use getSecret function to pass a key vault secret into a module. For the example below, we have two Bicep files:

When examining the code below, pay attention to these points:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ============ main.bicep ============

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: 'kv-contoso'
  // scope: resourceGroup('rg-contoso')   - if key vault is in a different resource group
}

module db './sqldb.bicep' = {
  name: 'sqlDbDeployment1'
  params: {
    myPassword: keyVault.getSecret('mySqlPassword')
    // myPassword: keyVault.getSecret('mySqlPassword', '2cc1676124b77bc9a1bfd30d8f4b6225')
  }
}

Running bicep build main.bicep produces the following ARM template where everything is compiled into one template. Note that at lines 15-21 it uses the same syntax to reference the key vault secret as we discussed in the Key Vault Secrets Through Parameter File section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "resources": [
    {
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2019-10-01",
      "name": "sqlDbDeployment1",
      "properties": {
        "expressionEvaluationOptions": {
          "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {
          "myPassword": {
            "reference": {
              "keyVault": {
                "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'rg-contoso'), 'Microsoft.KeyVault/vaults', 'kv-contoso')]"
              },
              "secretName": "mySqlPassword"
            }
          }
        },
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "parameters": {
            "myPassword": {
              "type": "secureString"
            }
          },
          "resources": [
            {
              "type": "Microsoft.Sql/servers/databases",
              "apiVersion": "2021-02-01-preview",
              "name": "[format('{0}/{1}', 'ContosoSqlServer', 'contosodb')]",
              "location": "westus",
              "dependsOn": [
                "[resourceId('Microsoft.Sql/servers', 'ContosoSqlServer')]"
              ]
            },
            {
              "type": "Microsoft.Sql/servers",
              "apiVersion": "2021-02-01-preview",
              "name": "ContosoSqlServer",
              "location": "westus",
              "properties": {
                "administratorLogin": "contosoadmin",
                "administratorLoginPassword": "[parameters('myPassword')]"
              }
            }
          ]
        }
      }
    }
  ]
}

How To Pass Dynamic Number Of Key Vault Secrets As Parameter Array

In the previous sections of this post, we discussed how to pass one key vault secret into a template or a module. This is of course useful, however, sometimes we want to pass multiple or even many key vault secrets into a template. Keeping each secret as a separate parameter will result in a lot of repetitive code which is better to avoid.

The approach we discuss in this section won’t work for all possible use cases, but if secrets usage can be split in smaller units, this problem can be elegantly resolved using the knowledge we already got from the previous sections. For example, it could be a deployment of multiple resources where each resource requires a few secrets. In such cases, it makes sense to pass resources as an array and use a loop in the template.

To pass a dynamic/large number of key vault secrets into a bicep file, secrets’ metadata (names, versions and their key vaults) should be passed into a template as an array. Then each key vault secret is passed in a loop into a module as a secure parameter using getSecret function.

Basically, we are going to leverage nested deployments (modules) to pass key vault secrets. We will discuss two cases: when all secrets come from the same or from different key vaults.

NOTE: Another less favorable but possible solution is to store multiple secrets in one key vault secret in a form of JSON, for example. Then in the template the value can be parsed into an object or array and used. But we are not discussing this approach.

Secrets From The Same Key Vault

When all secrets live in the same vault, it is a little bit easier compared when multiple key vaults involved. Here, we pass metadata about the key vault and secrets through three parameters:

The code below should illustrate the basic idea how to pass a dynamic number secrets into Azure Bicep deployment. Using this knowledge, we can explore more complex use cases, for example, how to combine secrets from multiple vaults.

NOTES:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ========== single-kv-secrets.bicep ==========

param keyVaultName string = 'kv-contoso'
param keyVaultResourceGroup string = 'rg-contoso'
param secrets array = [
  {
    name: 'secret1'
    version: '44c79bd8ccee40c3b720e43d1a6df0ba'
  }
  {
    name: 'secret2'
    version: ''
  }
]

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: keyVaultName
  scope: resourceGroup(keyVaultResourceGroup)
}

module modules 'resource.bicep' = [for secret in secrets: {
  name: 'moduleDeployment-${secret.name}'
  params: {
    mySecretValue: keyVault.getSecret(secret.name, secret.version)
  }
}]
// ========== resource.bicep ==========

@secure()
param mySecretValue string

// ...

Secrets From Different Key Vaults

When our secrets live in different key vaults, the code becomes a bit more complicated compared to the last section because we cannot create symbolic names for each key vault inside of the for-loop. However, this can be solved by adding one more level of nested deployment (module).

In the following template, we are reusing single-kv-secrets.bicep and resource.bicep files from Secrets From The Same Key Vault section as is without any changes. What we do is we group secrets by their corresponding key vault and use them together in a module, see code below.

// ========== main.bicep ==========

param secretsConfig array = [
  {
    keyVaultName: 'kv-contoso1'
    keyVaultResourceGroup: 'rg-contoso1'
    secrets: [
      {
        name: 'secret1'
        version: '44c79bd8ccee40c3b720e43d1a6df0ba'
      }
    ]
  }
  {
    keyVaultName: 'kv-contoso2'
    keyVaultResourceGroup: 'rg-contoso2'
    secrets: [
      {
        name: 'secret2'
        version: ''
      }
    ]
  }
]

module singleKVs 'single-kv-secrets.bicep' = [for kvSecrets in secretsConfig: {
  name: 'kvSecretsDeployment-${kvSecrets.keyVaultName}'
  params: {
    keyVaultName: kvSecrets.keyVaultName
    keyVaultResourceGroup: kvSecrets.keyVaultResourceGroup
    secrets: kvSecrets.secrets
  }
}]