Secure Azure deployments with Bicep and Azure Key Vault
Tags: devops, azure, security
Published: 2021-03-02 00:32:01

Bicep is a new (experimental) way for building and deploying infrastructure to Azure. It's a language that compiles down to standard Azure Resource Manager json templates. So instead of handcrafting hundreds (if not thousands) of lines of json, you can code it in Bicep and then let the compiler do the hard work :)

To start with Bicep, you need to install the [Bicep CLI tool](https://github.com/Azure/bicep#get-started-with-bicep) and ideally the [VS Code extension](https://github.com/Azure/bicep/blob/main/docs/installing.md#bicep-vs-code-extension) so that you can get Intellisense and code syntax colorization. Side note: not all theme extensions support Bicep yet.

If you want to see a video of me setting it all up and writing my first Bicep program, check out this recording:

{% youtube 36-lQTycZak %}

## Our first Bicep template
For the purpose of this blog, we'll use Bicep to create a SQL Server and SQL database resource on Azure. Create a new directory and add a `main.bicep` file:

```
param subscriptionId string = 'e42acc2b-8462-4fb5-bf0d-d983c0017584'
param kvResourceGroup string = 'identity'
param kvName string = 'cm-identity-kv'

resource kv 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: kvName
  scope: resourceGroup(subscriptionId, kvResourceGroup )
}

module sql './sql.bicep' = {
  name: 'deploySQL'
  params: {
    administratorLogin: kv.getSecret('sqlAdministratorLogin')
    administratorLoginPassword: kv.getSecret('sqlAdministratorLoginPassword')
  }
}

```

In this file, we define a few parameters that mainly point to our KeyVault resource and then we call into the SQL module. 

Next, we need to create a new Bicep file for our SQL Server and database. Name the new file `sql.bicep` and add the following code:

```
param serverName string = uniqueString('sql125486asdf2')
param sqlDBName string = 'SampleDB'
param location string = resourceGroup().location

@secure()
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'
  }
}
```

We will use a random name for the SQL Server and we'll inherit some properties (i.e the location) from the Azure resource group we're deploying to. `uniqueString()` and `resourceGroup()` are built-in Bicep functions. You can find more about Bicep functions [here](https://github.com/Azure/bicep/blob/main/docs/tutorial/03-using-expressions.md)

You'll notice that we also have two uninitialized **secure** parameters:
- administratorLogin
- administratorLoginPassword

We left them blank for a good reason! We don't want to store secrets in our code as this is a security attack vector. The values for these properties will be passed from the `main.bicep` file which will retrieve them from Azure KeyVault making our deployment code secure end-to-end. 

Let's build our code:
```
bicep build main.bicep
```
This should generate an output file `main.json` that contains the compiled ARM template json:

```
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "serverName": {
      "type": "string",
      "defaultValue": "[uniqueString('sql125486asdf')]"
    },
    "sqlDBName": {
      "type": "string",
      "defaultValue": "SampleDB"
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "administratorLogin": {
      "type": "string"
    },
    "administratorLoginPassword": {
      "type": "secureString"
    }
  },
  "functions": [],
  "resources": [
    {
      "type": "Microsoft.Sql/servers",
      "apiVersion": "2019-06-01-preview",
      "name": "[parameters('serverName')]",
      "location": "[parameters('location')]",
      "properties": {
        "administratorLogin": "[parameters('administratorLogin')]",
        "administratorLoginPassword": "[parameters('administratorLoginPassword')]"
      }
    },
    {
      "type": "Microsoft.Sql/servers/databases",
      "apiVersion": "2020-08-01-preview",
      "name": "[format('{0}/{1}', parameters('serverName'), parameters('sqlDBName'))]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Standard",
        "tier": "Standard"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Sql/servers', parameters('serverName'))]"
      ]
    }
  ]
}
```

## Let's add the Sec in DevSecOps

First and foremost, we need to ensure that the account deploying this ARM template has the right enough privilege to do so. You don't want to use a user account as these tend to have elevated permissions and can cause some serious damage. The recommended practice is to use a Service Principal account. You can create these via the Azure Portal, PowerShell or the CLI. In this instance, we'll use the Azure CLI

```
az login
az ad sp create-for-rbac --role Contributor --scopes /subscriptions/<Your Subscription ID>/resourceGroups/<Your Resource Group Name> --name <Give it a nice name> 
```
Take a note of the output.

For our account to also be able to retrieve the necessary secrets from Azure Key Vault, we need to create a very specific role. You can find more information about how to create this very custom RBAC role [here](https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/key-vault-parameter?tabs=azure-cli#grant-access-to-the-secrets).

Following the above docs, we'll create a new `keyvaultrole.json` file and paste the following json:

```
{
  "Name": "Key Vault CM resource manager template deployment operator",
  "IsCustom": true,
  "Description": "Lets you deploy a resource manager template with the access to the secrets in the Key Vault.",
  "Actions": [
    "Microsoft.KeyVault/vaults/deploy/action"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": [
"/subscriptions/<Your Subscription ID>/resourceGroups/<Your Resource Group Name>/providers/Microsoft.KeyVault/vaults/<Your Key Vault name>"
  ]
}
```
We now need to create this role in our Azure Subscription (if one doesn't exist already) and assign it to our Service Principal. In the Azure CLI, type the following:

```
az role definition create --role-definition ./keyvaultrole.json
az role assignment create --role "Key Vault CM resource manager template deployment operator" --assignee <your SP client Id> --resource-group bicep-demo
```

Now that we have an account with the right privileges, we need to sign out (as ourselves) and sign in with the newly created Service Principal account:

```
az account clear
az login --service-principal -u <Service Principal Id> -p <Service Principal password> --tenant <Azure AD Tenant ID>
az account show
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/acrmmr4kuvezttsrn1fb.png)

The final step is to configure our Key Vault to allow ARM deployments as well as create the necessary secrets that will be referenced by our Bicep-generated ARM templates

In Key Vault, navigate to the **Access Policies** tab and ensure that the **Azure Resource Manager for template deployment** option under **Enable Access to:** is checked. Make sure to press the **Save** button if you make any changes:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94hy9m0uoy0vhat75for.png)

Finally, we want to check that our Service Principal has been assigned the right role to be able to read secrets from Key Vault. Navigate to the **Access Control (IAM)** tab and check under **Role Assignments** to ensure that the script we run earlier worked:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dcpugu0kpgc18n763xi.png)

We now have a locked down account and the all the secrets stored in Key Vault. Let's finalize our Bicep/ARM deployment. We can now go ahead and deploy our infrastructure as code using the Azure CLI:

```
az deployment group create --resource-group bicep-demo --template-file main.bicep
```

The output will be a bunch of json with information about the deployment like the heavily omitted output below:

```
{
  "id": "/subscriptions/e42acc2d-8462-4fb5-bf0d-d983c0017584/resourceGroups/bicep-demo/providers/Microsoft.Resources/deployments/cmdeployment425show",
  "location": null,
    ...
    "provisioningState": "Succeeded",
    "templateHash": "18120650064530837360",
    "templateLink": null,
    "timestamp": "2021-03-02T00:16:56.630918+00:00",
    "validatedResources": null
  },
  "resourceGroup": "bicep-demo",
  "tags": null,
  "type": "Microsoft.Resources/deployments"
}
```

## Building more elaborate Bicep code
The GitHub repo is thin at the moment and for a moment I thought I was stuck with the tutorial sample code. But then, I came across the glorious **Bicep Playground** and all was great again. The [Playground](https://bicepdemo.z22.web.core.windows.net/) displays Bicep and compiled json side by side and provides you with a lot of sample code to help you build your own templates. My suspicion is that this is all driven by the Azure ARM Samples repo:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s73olj7fb6s0k8ojvm9i.png)

## Source Code
This [GitHub repo](https://github.com/425show/BicepDemo) contains all the files and code referenced in this blob post.

## Summary
I love Bicep because it makes working with Azure Resources more straightforward and concise. And unlike Pulumi or Terraform, it's native Azure and therefore provides better support for Azure resources. However, there are pros and cons with every tool so choose the one that truly makes you happy (or productive).