Azure Bicep: Secure secrets in parameters — @secure() decorator

Ensure you protect the secrets referenced from parameters in your Bicep code

This article will go over the use of parameters in Bicep and how we can protect them using the @secure() decorator.

If you’re reading this article, I bet you have been in a situation where you need to deploy a virtual machine, a database, or a resource that requires you to pass on a secret like a password.

These secrets can be passed on as parameters in your Bicep template, and it is best to use specific types to protect these values. By default, parameters are not protected, so what options do we have to pass on secret values in Bicep?

Working with secrets in Parameters

ARM templates provide two data types to pass on secret values in parameters: ‘secureString’ and ‘secureObject’. These data types are not available in Bicep, but you can leverage a @secure() decorator.

Decorators in Bicep provide a way to attach constraints and metadata to a parameter. These decorators are defined above the parameter declaration and use the ‘@expression’. This expression is basically a function call.

The code below shows an example of how to use a declaration with decorators:

@expression
param myParam string

In the above code, we define the decorator and the parameter string type in the next line. We can use decorators to secure parameters in Bicep.

Secure parameters in Bicep

As mentioned before, the data types ‘secureString’ and ‘secureObject’ are not available in Bicep; therefore, we will leverage the @secure() decorator as shown below:

@secure()
param myPassword string
@secure()
param mySuperSecretObject object

This definition will transpile into the well known ‘secureString’ and ‘secureObject’ as shown below:

Azure Bicep — Secure parameter

You also have other options to leverage the @secure() decorator:

@secure()
param adminPassword string
@secure()
param adminPassword string = ''
@secure()
param adminPassword string = newGuid()

It is recommended not to hard-code the default value for a secure parameter in your Bicep template unless it is empty or use an expression containing a call to newGuid() as shown in the above code.

Now let’s review the following example where we will deploy a SQL database. The following example will help you better understand how the @secure()decorator works.

Example. Deploy SQL database

The Bicep template below creates a SQL database and makes use of the @secure() decorator.

param serverName string = uniqueString('sql', resourceGroup().id)
param sqlDBName string = 'AzInsiderDb'
param location string = resourceGroup().location
param administratorLogin string
@secure()
param administratorLoginPassword string
resource server 'Microsoft.Sql/servers@2019-06-01-preview' = {
name: serverName
location: location
properties: {
administratorLogin: administratorLogin
administratorLoginPassword: administratorLoginPassword
}
}
resource sqlDB 'Microsoft.Sql/servers/databases@2020-08-01-preview' = {
name: '${server.name}/${sqlDBName}'
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
}

In the above code, we define a few parameters for the server name, the SQL database name, the location, and the admin login. Then we use the @secure() decorator for the admin password.

Next, we will deploy the Bicep template to a resource group previously created with the command below:

New-AzResourceGroupDeployment -Name $deploymentName -ResourceGroupName sql-database -TemplateFile .\main.bicep

During deployment time, we will provide the administrator login and the password as shown in the figure below:

Azure Bicep — Deploy SQL Database

Similar to ARM templates, the actual value of the secret is never exposed. It is recommended to use this type of parameter for your passwords and secrets.

Sensitive data passed on as a secure parameter can’t be read after resource deployment and isn’t logged.

The image below shows the output from the SQL database deployment:

Azure Bicep — SQL database deployment output

Another common use case is the deployment of virtual machines. In the following example, we will deploy a Windows-based virtual machine using Bicep and the @secure() decorator.

Example. Deploy Windows-based virtual machine using @secure() decorator

In the code below, we will use a few parameters:

param adminUserName string
param dnsLabelPrefix string
param windowsOSVersion string = '2019-Datacenter'
param vmSize string = 'Standard_D2_v3'
param location string = resourceGroup().location

Then we will define the parameter for the password of the virtual machine:

@secure()
param adminPassword string

Note that we are not hardcoding the password. The next step is to define a few variables related to the networking components, the name of the virtual machine, and the storage account.

var storageAccountName = 'drendonwinvmsa'
var nicName = 'myVMNic'
var addressPrefix = '10.0.0.0/16'
var subnetName = 'Subnet'
var subnetPrefix = '10.0.0.0/24'
var publicIPAddressName = 'myPublicIP'
var vmName = 'winVm'
var virtualNetworkName = 'MyVNET'
var subnetRef = '${vn.id}/subnets/${subnetName}'
var networkSecurityGroupName = 'default-NSG'

Then, we will define the resources needed to deploy the Windows-based virtual machine:

resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'Storage'
}
resource pip 'Microsoft.Network/publicIPAddresses@2020-06-01' = {
name: publicIPAddressName
location: location
properties: {
publicIPAllocationMethod: 'Dynamic'
dnsSettings: {
domainNameLabel: dnsLabelPrefix
}
}
}
resource sg 'Microsoft.Network/networkSecurityGroups@2020-06-01' = {
name: networkSecurityGroupName
location: location
properties: {
securityRules: [
{
name: 'default-allow-3389'
'properties': {
priority: 1000
access: 'Allow'
direction: 'Inbound'
destinationPortRange: '3389'
protocol: 'Tcp'
sourcePortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = {
name: virtualNetworkName
location: location
properties: {
addressSpace: {
addressPrefixes: [
addressPrefix
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: subnetPrefix
networkSecurityGroup: {
id: sg.id
}
}
}
]
}
}
resource nInter 'Microsoft.Network/networkInterfaces@2020-06-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: pip.id
}
subnet: {
id: subnetRef
}
}
}
]
}
}
resource VM 'Microsoft.Compute/virtualMachines@2020-06-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: vmSize
}
osProfile: {
computerName: vmName
adminUsername: adminUserName
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: windowsOSVersion
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
}
dataDisks: [
{
diskSizeGB: 1023
lun: 0
createOption: 'Empty'
}
]
}
networkProfile: {
networkInterfaces: [
{
id: nInter.id
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
storageUri: stg.properties.primaryEndpoints.blob
}
}
}
}

Lastly, we will include an output to show the virtual machine hostname:

output hostname string = pip.properties.dnsSettings.fqdn

Before we perform the actual deployment we can have a preview of the deployment using the flag -C with the command below:

New-AzResourceGroupDeployment -Name $deploymentName -ResourceGroupName winvm -TemplateFile .\main.bicep -c

The figure below shows the deployment preview:

Azure Bicep — deployment preview

Once the Bicep template looks good to you, we can confirm the deployment. The figure below shows the output from this deployment:

Azure Deployment output

Here’s the complete Bicep template:

param adminUserName string
param dnsLabelPrefix string
param windowsOSVersion string = '2019-Datacenter'
param vmSize string = 'Standard_D2_v3'
param location string = resourceGroup().location
@secure()
param adminPassword string
var storageAccountName = 'drendonwinvmsa'
var nicName = 'myVMNic'
var addressPrefix = '10.0.0.0/16'
var subnetName = 'Subnet'
var subnetPrefix = '10.0.0.0/24'
var publicIPAddressName = 'myPublicIP'
var vmName = 'winVm'
var virtualNetworkName = 'MyVNET'
var subnetRef = '${vn.id}/subnets/${subnetName}'
var networkSecurityGroupName = 'default-NSG'
resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'Storage'
}
resource pip 'Microsoft.Network/publicIPAddresses@2020-06-01' = {
name: publicIPAddressName
location: location
properties: {
publicIPAllocationMethod: 'Dynamic'
dnsSettings: {
domainNameLabel: dnsLabelPrefix
}
}
}
resource sg 'Microsoft.Network/networkSecurityGroups@2020-06-01' = {
name: networkSecurityGroupName
location: location
properties: {
securityRules: [
{
name: 'default-allow-3389'
'properties': {
priority: 1000
access: 'Allow'
direction: 'Inbound'
destinationPortRange: '3389'
protocol: 'Tcp'
sourcePortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = {
name: virtualNetworkName
location: location
properties: {
addressSpace: {
addressPrefixes: [
addressPrefix
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: subnetPrefix
networkSecurityGroup: {
id: sg.id
}
}
}
]
}
}
resource nInter 'Microsoft.Network/networkInterfaces@2020-06-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: pip.id
}
subnet: {
id: subnetRef
}
}
}
]
}
}
resource VM 'Microsoft.Compute/virtualMachines@2020-06-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: vmSize
}
osProfile: {
computerName: vmName
adminUsername: adminUserName
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
publisher: 'MicrosoftWindowsServer'
offer: 'WindowsServer'
sku: windowsOSVersion
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
}
dataDisks: [
{
diskSizeGB: 1023
lun: 0
createOption: 'Empty'
}
]
}
networkProfile: {
networkInterfaces: [
{
id: nInter.id
}
]
}
diagnosticsProfile: {
bootDiagnostics: {
enabled: true
storageUri: stg.properties.primaryEndpoints.blob
}
}
}
}
output hostname string = pip.properties.dnsSettings.fqdn

Hopefully, with these examples, you can better understand how you can leverage the Bicep capabilities to protect sensitive data in parameters.

Join the AzInsider email list here.

-Dave R.

--

--