Azure, Bicep, DevOps, IaC, Security

Security Fundamentals with Bicep

Being a Snyk Ambassador has been fun so far and last year I got this very article published on the Snyk Blog and so thought I would also share it here on my personal blog.

If you want to know more about the Snyk Ambassador Program head over to the website its a great way to meet like minded people who have a passion for application security.

So on to the post, you can click the link to the Snyk Blog or you can continue reading 🙂

Azure Bicep is getting more popular by the day and is rapidly becoming the replacement for ARM templates. In this post I am going to go over some Security Fundamentals when using Bicep.

If you are not familiar with Bicep then I recommend taking a look at the Microsoft Learn Documentation to find out more.

Keep Secrets out of Source Control

We all know we want to keep our secrets out of source control, but it is very easy to accidently leave secrets in files especially when testing out your Bicep configurations locally.

Some ways to avoid this are:

  • Pass parameters in via command line
  • Use a parameters JSON file that is ignored by source control. For example add them to your .gitignore file (if you are using Git)

Secure Inputs

Passing in parameters from the outside is one thing but how do you make sure secrets are secure and not displayed in outputs?

Bicep provides an @secure decorator for String and Object type parameters e.g.

@secure()
param adminPassword string

@secure()
param adminCredentials object

Be Careful of Outputs

Adding outputs to your Bicep modules is very useful but there are a few things to be aware of:

If you are setting an output that looks like a secret then Bicep will provide a warning that you are exposing potential secrets.

The following output for a connection string to a Storage Account would output such a warning

output connection string = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}'

However, if the value was added to a variable before being assigned to the output, then no warning would be shown and would be easy to miss

var connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}'

output connection string = connectionString

Now let’s see what happens if a storage account resource is deployed to Azure using the following configuration

deploy.bicep

param location string = resourceGroup().location
param tags object = {}
param storageName string = 'stsecureteststore'
param sku string = 'Standard_LRS'

module storageModule 'modules/storage.bicep' = {
  name: 'StorageDeploy'
  params: {
    location: location
    storageName: storageName
    tags: tags
    sku: sku
  }
}

modules/storage.bicep

@description('The storage account name')
@minLength(3)
@maxLength(24)
param storageName string
@description('The storage account location')
param location string
@description('The tags for the storage account')
param tags object
@description('The storage account sku') 
@allowed([ 'Standard_LRS', 'Standard_GRS', 'Standard_GZRS', 'Standard_RAGRS', 'Standard_RAGZRS', 'Standard_ZRS', 'Premium_LRS', 'Premium_ZRS' ])
param sku string = 'Standard_LRS'
@description('The access tier for the blob services') 
@allowed([ 'Hot', 'Cool' ]) 
param accessTier string = 'Hot' 
@description('Allow public access to blobs') 
param allowBlobPublicAccess bool = false 

resource storageaccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
  name: storageName
  location: location
  kind: 'StorageV2'
  tags: tags
  sku: {
    name: sku
  }
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    accessTier: accessTier
    allowBlobPublicAccess: allowBlobPublicAccess
  }
}

var connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageaccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}'
output connection string = connectionString

Any outputs defined in Bicep can be seen as under Deployments for the resource group the resources have been deployed to

Looking at the StorageDeploy outputs the connection is shown including the account key in plain text

This means anyone with Access to view the resources in the Azure Portal can see these outputs. To maintain a good security posture, it is recommended to not return secrets as outputs in Bicep.

Hopefully Bicep will support the use of the @secure decorator for outputs in the future to make returning secrets safe and secure.

Secrets from Resources

If returning secrets from Bicep is a problem, then how do you get secrets from one module to another?

One option is to access an existing resource by using the existing keyword e.g.

param storageName string

resource storageaccount 'Microsoft.Storage/storageAccounts@2022-05-01' existing = {
  name: storageName  
}

var connectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageaccount.id, storageaccount.apiVersion).keys[0].value}'

This connection string could then be used as an input for another resource.

Secrets from Key Vault

Getting existing resources is one way of getting secrets but there is also support for using a Key Vault to retrieve secrets

Note: Make sure that the Key Vault Access Configuration allows access via “Azure Resource Manager for template deployment”

Key Vaults are accessed in the same way as in the previous section, by use of the existing keyword. One caveat to note however is that getSecret method can only be used when assigning to a module parameter with the @secure decorator e.g.

deploy.bicep

param location string = resourceGroup().location
param tags object
param sqlServerName string
param keyVaultName string
param keyVaultResourceGroupName string
param subscriptionId string = subscription().subscriptionId

resource vaultResource 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName 
  scope: resourceGroup(subscriptionId, keyVaultResourceGroupName  )
}

module sqlModule 'modules/sql.bicep' = {
  name: 'SqlDeploy'
  params: {
    location: location
    tags: tags
    sqlServerName: sqlServerName
    administratorLogin: vaultResource.getSecret('sqlUser')
    administratorLoginPassword: vaultResource.getSecret('sqlPassword')
  }  
}

modules/sql.bicep

@description('The resource location')
param location string
@description('The tags for the resources')
param tags object
@description('The name for the SQL Server')
param sqlServerName string
@secure()
@description('The SQL Administrator Login')
param administratorLogin string
@secure()
@description('The SQL Administrator password')
param administratorLoginPassword string

resource sqlServerResource 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: sqlServerName
  location: location
  tags:tags
  properties: {
    administratorLogin: administratorLogin
    administratorLoginPassword: administratorLoginPassword
  }
}

Scanning Bicep

Scanning of IaC (Infrastructure as Code) is becoming quite popular and it is good to see that there is interest in finding security issues as early as possible.

 Snyk has a free CLI that can be used to perform IaC scans locally against security and compliance standards. While  it does not directly support the Bicep format, it does support scanning of ARM templates that Bicep compiles down to.

To compile Bicep to ARM you need to have the Bicep CLI installed, and to get started with the Snyk CLI create a free account and then install Snyk CLI using npm. If you have Node.js installed locally, you can install it by running:

npm install snyk@latest -g

Once installed and setup you can then run the command

az bicep build -f {file_name}.bicep

This will produce a JSON file with the same name as the Bicep file and then you can run the Snyk Scan with the command

snyk iac test {file_name}.json

Final Thoughts

Security is something we all have to think about and it’s a constant moving target but the more we learn the more we can do to help secure our resources. I hope this post has been informative and provided some insights to securing your Bicep configurations.

Azure, Bicep, DevOps, IaC

Bicep – Variable JSON Files

Introduction

Recently I was looking into others ways to provide inputs to Bicep. In previous posts I have used various techniques to pass parameters in via Azure Pipelines, either updating a parameters JSON file or creating the whole file in the pipeline (I bundled those techniques into one post if you want to take a look).

If you are not sure about creating a parameters file to use with Bicep I recommend the Visual Studio Code extension Azure Resource Manager (ARM) Tools as this provides an easy way to create them. Also there is a PowerShell module written by Stefan Ivemo (and others) that has a command ‘New-BicepParameterFile’ that creates a parameter file from a Bicep file (see github and/or the PowerShell gallery)

In other Infrastructure as Code (IaC) frameworks like Terraform, there are ways to load variable files in and they are usually in the same language as the framework. Bicep however doesn’t currently support variables files in the Bicep language, but as I discovered, it does have a function called loadTextContent, this coupled with the json function allows JSON files to be loaded in and therefore could be used as variable files.

So, in this post I am going to explore using these functions to load variables into Bicep.

NOTE: I will be using az bicep version 0.6.1

The Code

Let’s start with something small like creating a storage account.

First create some settings for the account, name, sku, etc. in a JSON file (vars_storage.json) e.g.

{
    "name": "stmvarsdemodevweu",
    "sku": "Standard_LRS",
    "kind": "StorageV2",
    "properties": {
        "minimumTlsVersion": "TLS1_2"
    }
}

Next create the bicep file (deploy.bicep) to create the storage account and load in the variables from the json file using loadTextContent and json functions:

param location string = resourceGroup().location 
var storageConfig = json(loadTextContent('vars_storage.json'))

resource storageaccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: storageConfig.name
  location: location
  kind: storageConfig.kind
  sku: {
    name: storageConfig.sku
  }
  properties: {
    minimumTlsVersion: storageConfig.properties.minimumTlsVersion
  }
}

NOTE: With the Visual Studio Code Bicep extension you get intellisense on the json properties as well which is really nice

I already created the resource group in Azure and so just need to deploy the Bicep via the Azure CLI

 az deployment group create --resource-group rg-varsdemo-dev-weu --template-file deploy.bicep

Now, what if we wanted multiple variable files? as you can imagine you can just load in another file.

Let’s add a Virtual network to Bicep.

As before, starting with the JSON file (vars_network.json) define the network configuration

{
    "name": "vnet-varsdemo-dev-weu",
    "addressPrefixes": [
        "10.72.0.0/16"
    ],
    "subnets": [
        {
            "name": "Subnet-1",
            "properties": {
                "addressPrefix": "10.72.252.0/24"
            }
        },
        {
            "name": "Subnet-2",
            "properties": {
                "addressPrefix": "10.72.254.0/24"
            }
        }
    ]
}

And then add the Virtual Network to Bicep and load in the new json file into a variable

param location string = resourceGroup().location 
var storageConfig = json(loadTextContent('vars_storage.json'))
var networkConfig = json(loadTextContent('vars_network.json'))

resource storageaccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: storageConfig.name
  location: location
  kind: storageConfig.kind
  sku: {
    name: storageConfig.sku
  }
  properties: {
    minimumTlsVersion: storageConfig.properties.minimumTlsVersion
  }
}

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = {
  name: networkConfig.name
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: networkConfig.addressPrefixes
    }
    subnets: networkConfig.subnets
  }
}

Multiple Environments

Often when we use IaC we are deploying to multiple environments. Can files be dynamically loaded by a parameter value? something like this?

param location string = resourceGroup().location
param env string
var storageConfig = json(loadTextContent('vars_${env}_storage.json'))
var networkConfig = json(loadTextContent('vars_${env}_network.json'))

Unfortunately this is not a supported configuration in Bicep

The reason for this is that Param is determined at runtime and loadTextContent is determined at compile time

It is possible however to define environments in the file and load the desired section at runtime.

NOTE: One downside of using this way of loading environments is that Visual Studio Code loses intellisense on the JSON properties due to the runtime env parameter

Let’s change the JSON files to support multiple environments

vars_storage.json

{
    "dev": {
        "name": "stmvarsdemodevweu",
        "sku": "Standard_LRS",
        "kind": "StorageV2",
        "properties": {
            "minimumTlsVersion": "TLS1_2"
        }
    },
    "prod": {
        "name": "stmvarsdemoprodweu",
        "sku": "Premium_LRS",
        "kind": "StorageV2",
        "properties": {
            "minimumTlsVersion": "TLS1_2"
        }
    }
}

vars_network.json

{
    "dev": {
        "name": "vnet-varsdemo-dev-weu",
        "addressPrefixes": [
            "10.72.0.0/16"
        ],
        "subnets": [
            {
                "name": "Subnet-1",
                "properties": {
                    "addressPrefix": "10.72.252.0/24"
                }
            },
            {
                "name": "Subnet-2",
                "properties": {
                    "addressPrefix": "10.72.254.0/24"
                }
            }
        ]
    },
    "prod": {
        "name": "vnet-varsdemo-dev-weu",
        "addressPrefixes": [
            "10.74.0.0/16"
        ],
        "subnets": [
            {
                "name": "Subnet-1",
                "properties": {
                    "addressPrefix": "10.74.252.0/24"
                }
            },
            {
                "name": "Subnet-2",
                "properties": {
                    "addressPrefix": "10.74.254.0/24"
                }
            }
        ]
    }
}

And finally add the loading of the environment in Bicep

param location string = resourceGroup().location
param env string
var storageConfig = json(loadTextContent('vars_storage.json'))['${env}'] 
var networkConfig = json(loadTextContent('vars_network.json'))['${env}'] 

resource storageaccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: storageConfig.name
  location: location
  kind: storageConfig.kind
  sku: {
    name: storageConfig.sku
  }
  properties: {
    minimumTlsVersion: storageConfig.properties.minimumTlsVersion
  }
}

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = {
  name: networkConfig.name
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: networkConfig.addressPrefixes
    }
    subnets: networkConfig.subnets
  }
}

As before this can now be deployed into Azure via the Azure CLI adding the env parameter

Dev

az deployment group create --resource-group rg-varsdemo-dev-weu --template-file deploy.bicep --parameters env=dev

Prod

az deployment group create --resource-group rg-varsdemo-prod-weu --template-file deploy.bicep --parameters env=prod

Conclusion

You can of course provide all the variables inside the Bicep file including multiple environments and that is a valid way of defining values. However if you have a lot of variables, using a separate file whether that be a parameters file or following this technique can reduce the noise inside the Bicep file. Separate files also allow values to be overridden by a CI/CD pipeline, share variables between multiple Bicep files and it might be easier to maintain configuration if it is in separate files.

I think loading files in is a nice way of injecting values into Bicep, its a shame that other file formats like YAML are not currently supported or using Bicep style variable/parameters files. Hopefully these features will become part of Bicep in the future.

Well I hope you found this useful and discovered another way to get inputs into Bicep. Bicep is certainly improving more and more and I look forward to future releases.

Happy IaCing !!