Creating Azure Key Vault with logging using Azure Resource Manager ARM templates

If you are looking to create an Azure Key Vault, have full visibility/traceability over who access it and what they did while preventing accidental deletes of your vault, you are at the right place. Let me show you how to achieve this using well-known best practices and patterns like template linking in Azure Resource Manager templates.

What this solution do?

  • Create Key vault
  • Create Storage Account
  • Enable Key Vault logging on storage account
  • Optionally creates resource locks on Key Vault and Storage Account to protect them.

What Is Azure Key Vault?

Key Vault help you safeguard cryptographic keys and other secrets used by your applications whenever they are On-Premise or in the cloud. More and more services on Azure are now integrating Azure Key Vault as their secret/key source for things like deployments, data or even disk encryption.

Assumptions
This article assumes you have basic knowledge and experience with ARM templates. That you know how to configure a resource type in the Resource section of a template. If you don't and need more information about ARM templates, please take a look here before going further: Authoring Azure Resource Manager templates

Why do all this in templates when we can script it?

A huge benefit of ARM templates is that they are idempotent. So if something fails, you can re-run them without having to worry about cleaning up first. They are mostly optimized for what the state should be, rather than how to get there... Also because I love challenges ;) It's true that you can do all of this in PowerShell or Azure-CLI but I wanted to prove myself that I could do it all using an ARM template without much existing documentation. After a lot of trials & errors, I found out how to do it about a year ago. For many good and less good reasons, I didn't take the time to format and share that knowledge. This week I finally took the time to clean the recipe I had, pimp it a bit & share it on the Azure Resource Manager QuickStart Templates repository on GitHub. It is an incredible source of knowledge about Azure Resource Manager templates. You will find the templates under 201-key-vault-with-logging-create.

What is Template linking?

This was taken from Microsoft documentation:

From within one Azure Resource Manager template, you can link to another template, which enables you to decompose your deployment into a set of targeted, purpose-specific templates. As with decomposing an application into several code classes, decomposition provides benefits in terms of testing, reuse, and readability.

You can pass parameters from a main template to a linked template, and those parameters can directly map to parameters or variables exposed by the calling template. The linked template can also pass an output variable back to the source template, enabling a two-way data exchange between templates.

You can see from this description that template linking is pretty useful when we want to decompose your deployment into a set of targeted, purpose-specific templates. It helps us with testing, reuse, and readability... but I will also add to the list: mimic conditional logic in your templates.

What is Resources locking?

This was taken from Microsoft documentation:

As an administrator, you may need to lock a subscription, resource group, or resource to prevent other users in your organization from accidentally deleting or modifying critical resources. You can set the lock level to CanNotDelete or ReadOnly.

It is important to understand that the lock levels are in the management(control) plane as explained below:

Resource Manager locks apply only to operations that happen in the management plane, which consists of operations sent to https://management.azure.com. The locks do not restrict how resources perform their own functions. Resource changes are restricted, but resource operations are not restricted. For example, a ReadOnly lock on a SQL Database prevents you from deleting or modifying the database, but it does not prevent you from creating, updating, or deleting data in the database. Data transactions are permitted because those operations are not sent to https://management.azure.com.

Digging into 201-key-vault-with-logging-create

The solution consists of 3 templates, one master that creates all the resources and two other templates for the conditional logic around resources locking. Only one of them will get called depending on if you want to protect your resources with locks or not.

Master template parameters

Here are the parameters you will find in the master template:

"parameters": {
  "keyVaultName": {
    "type": "string",
    "minLength": 1,
    "metadata": {
      "description": "KeyVault name"
    }
  },
  "accessPolicies": {
    "type": "array",
    "defaultValue": "{}",
    "metadata": {
      "description": "Access policies object {\"tenantId\":\"\",\"objectId\":\"\",\"permissions\":{\"keys\":[\"\"],\"secrets\":[\"\"]}}"
    }
  },
  "diagnosticStorageAccountName": {
    "type": "string",
    "metadata": {
      "description": "Specifies the name of the storage account where diagnostics logs will be written"
    }
  },
  "logsRetentionInDays": {
    "type": "int",
    "defaultValue": 0,
    "minValue": 0,
    "maxValue": 365,
    "metadata": {
      "description": "Specifies the number of days that logs are gonna be kept. If you do not want to apply any retention policy and retain data forever, set value to 0."
    }
  },
  "enableVaultForDeployment": {
    "type": "bool",
    "defaultValue": false,
    "allowedValues": [
      true,
      false
    ],
    "metadata": {
      "description": "Specifies if the vault is enabled for deployment by script or compute (VM, Service Fabric, ...)"
    }
  },
  "enableVaultForTemplateDeployment": {
    "type": "bool",
    "defaultValue": false,
    "allowedValues": [
      true,
      false
    ],
    "metadata": {
      "description": "Specifies if the vault is enabled for a template deployment"
    }
  },
  "enableVaultForDiskEncryption": {
    "type": "bool",
    "defaultValue": false,
    "allowedValues": [
      true,
      false
    ],
    "metadata": {
      "description": "Specifies if the azure platform has access to the vault for enabling disk encryption scenarios."
    }
  },
  "vaultSku": {
    "type": "string",
    "defaultValue": "Standard",
    "allowedValues": [
      "Standard",
      "Premium"
    ],
    "metadata": {
      "description": "Specifies the SKU for the vault"
    }
  },
  "protectWithLocks": {
    "type": "string",
    "allowedValues": [
      "enabled",
      "disabled"
    ],
    "defaultValue": "disabled"
  }
}

The parameters are pretty straightforward. What is interesting here is how we declare the protectWithLocks parameter and how we use it in the template. It is of type string and we allow only a restricted set of values, enabled and disabled. We need to have a parameter of type string & allowed values to achieve our goal, which is to mimic conditional logic (otherwise not possible out of the box) in templates.

Master template variables

We assemble a complex variable object named template. The base member is a static variable that contain the base URI of our templates on GitHub. The protectWithLocks member is a dynamic variable that use the concat template function to build the linked template name to call from the master template.

  1. 'nestedtemplates/protectwithlocks'
  2. 'enabled'|'disabled'
  3. '.json'
"variables": {
  "template": {
    "base": "https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/201-key-vault-with-logging-create/",
    "protectWithLocks": "[concat('nestedtemplates/protectwithlocks', parameters('protectWithLocks'), '.json')]"
  },
  "uniqueString": "[uniqueString(subscription().id, resourceGroup().id)]",
  "diagnosticStorageAccountName": "[toLower( substring( replace( concat( parameters('keyVaultName'), variables('uniqueString'), variables('uniqueString') ), '-', ''), 0, 23) )]"
}

If we set the protectWithLocks parameter to value enabled, the template variable will end up with the value nestedtemplates/protectwithlocksenabled.json for member protectWithLocks.

We also generate 24 unique characters value for the diagnosticStorageAccountName variable based on the keyVaultName parameter and unique values generated using the 'uniqueString' template function.

Master template resources

Key Vault

We define the Key Vault resource and also (this is where the magic hookup happens) the Key Vault logging using the resource type Microsoft.KeyVault/vaults/providers/diagnosticsettings.

{
  "type": "Microsoft.KeyVault/vaults",
  "name": "[parameters('keyVaultName')]",
  "apiVersion": "2016-10-01",
  "Location": "[resourceGroup().location]",
  "tags": {
    "displayName": "Key Vault with logging"
  },
  "properties": {
    "enabledForDeployment": "[parameters('enableVaultForDeployment')]",
    "enabledForTemplateDeployment": "[parameters('enableVaultForTemplateDeployment')]",
    "enabledForDiskEncryption": "[parameters('enableVaultForDiskEncryption')]",
    "tenantId": "[subscription().tenantId]",
    "accessPolicies": "[parameters('AccessPolicies')]",
    "sku": {
      "name": "[parameters('vaultSku')]",
      "family": "A"
    }
  },
  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults/providers/diagnosticsettings",
      "name": "[concat(parameters('keyVaultName'), '/Microsoft.Insights/service')]",
      "apiVersion": "2016-09-01",
      "Location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]",
        "[concat('Microsoft.Storage/storageAccounts/', variables('diagnosticStorageAccountName'))]"
      ],
      "properties": {
        "storageAccountId": "[resourceId('Microsoft.Storage/storageAccounts', variables('diagnosticStorageAccountName'))]",
        "logs": [
          {
            "category": "AuditEvent",
            "enabled": true,
            "retentionPolicy": {
              "enabled": true,
              "days": "[parameters('LogsRetentionInDays')]"
            }
          }
        ]
      }
    }
  ]
}

The name of the Key Vault logging resource needs to be the name of your vault appended with /Microsoft.Insights/service. We link to the desired storage account using the storageAccountId property. Using the parameter LogsRetentionInDays we control how long we want the logs to be kept. Allowed values are from 0 to 365, if you choose 0, logs will be kept indefinitely.

Storage Account

Nothing fancy here, we define the storage account that is used exclusively for Key Vault logging. It is a good practice to keep it isolated so you can restrict to a maximum who can access the information stored in there using RBAC & Shared Access Signatures (SAS tokens).

{
  "type": "Microsoft.Storage/storageAccounts",
  "name": "[variables('diagnosticStorageAccountName')]",
  "apiVersion": "2016-12-01",
  "Location": "[resourceGroup().location]",
  "sku": {
    "name": "Standard_LRS"
  },
  "kind": "Storage",
  "tags": {
    "displayName": "concat('Key Vault ', parameters('keyVaultName'), ' diagnostics storage account')"
  },
  "properties": {}
}

Master template linked template

This is where we call the resources locking template.

{
  "type": "Microsoft.Resources/deployments",
  "name": "protectWithLocks",
  "apiVersion": "2016-09-01",
  "properties": {
    "mode": "Incremental",
    "templateLink": {
      "uri": "[concat(variables('template').base, variables('template').protectWithLocks)]",
      "contentVersion": "1.0.0.0"
    },
    "parameters": {
      "keyVaultName": {
        "value": "[parameters('keyVaultName')]"
      },
      "diagnosticStorageAccountName": {
        "value": "[variables('diagnosticStorageAccountName')]"
      }
    }
  },
  "dependsOn": [
    "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]",
    "[concat('Microsoft.Storage/storageAccounts/', variables('diagnosticStorageAccountName'))]"
  ]
}

ProtectWithLocksDisabled template

This is an empty template that does nothing. As the name states, if we ended up calling this template it's because we don't want resource locks.

Note: If there are locks already present on the resources because of a previous deployment or manual intervention, we cannot undo this using a template without impacting all other resources in the resource group. In short, it will not remove locks once applied, you will need to do it yourself.

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "keyVaultName": {
            "type": "string",
            "minLength": 1,
            "metadata": {
                "description": "Specifies the name of the Key Vault to lock"
            }
        },
        "diagnosticStorageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Specifies the name of the storage account to lock"
            }
        }
    },
    "variables": {},
    "resources": [],
    "outputs": {}
}

ProtectWithLocksEnabled template

This template creates 2 resources locks. One for the vault and one for the vault logging storage account.

{
  "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "keyVaultName": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Specifies the name of the Key Vault to lock"
      }
    },
    "diagnosticStorageAccountName": {
      "type": "string",
      "metadata": {
        "description": "Specifies the name of the storage account to lock"
      }
    }
  },
  "variables": {},
  "resources": [
    {
      "comments": "Resource lock on Key Vault",
      "type": "Microsoft.KeyVault/vaults/providers/locks",
      "name": "[concat(parameters('keyVaultName'), '/Microsoft.Authorization/keyVaultDoNotDelete')]",
      "apiVersion": "2016-09-01",
      "properties": {
        "level": "CannotDelete"
      }
    },
    {
      "comments": "Resource lock on diagnostics storage account",
      "type": "Microsoft.Storage/storageAccounts/providers/locks",
      "name": "[concat(parameters('diagnosticStorageAccountName'), '/Microsoft.Authorization/storageDoNotDelete')]",
      "apiVersion": "2016-09-01",
      "properties": {
        "level": "CannotDelete"
      }
    }
  ],
  "outputs": {}
}

Hope this helps you in your journey for a safer use of sensitive information in the cloud!

A special thank you to Sumedh Barde of the Azure Key Vault team for his help reviewing this blog post.

Resources