Generate SAS Tokens in ARM Templates

Generate SAS Tokens in ARM Templates

Without much fanfare, MS recently updated the ARM template spec to allow the creation of SAS tokens inside a template. This is excellent news for anyone who is deploying resources with ARM templates that rely on storage accounts and need a SAS token to access them.

For those not familiar with SAS tokens, you can read more on them here but essentially Shared Access Signatures (SAS) provide a way to generate a key to undertake operations on a storage account, without needing the actual storage account key. The benefit of this is that these SAS tokens can be time-limited, permission limited and even IP limited, making it much more secure. It is very common to use SAS tokens for other users and applications to access a storage account.

Often, SAS tokens are generated programmatically by applications, but sometimes you need to use a token directly, in the app settings of a web app, as a Key Vault secret accessed by your function etc. Up till now, this has meant that you need to manually go and generate a SAS token after you deploy your storage account and then add it to the web app or Key Vault. This is no longer the case; we can now generate tokens at deploy time and add them to our app or vault in the same template, fully automated.

Create a SAS Token in a Deployment

We are going to look at a simple scenario, where we are deploying a Storage Account and a Key Vault, and we want to add a secret to the vault that is going to be consumed by our apps. Creating the Storage Account and Key Vault is no different than usual, the magic happens with creating the secret.

SAS Properties

Probably the most complicated thing about creating the SAS token (and it's not that complex) is getting the properties right. If you have ever created a SAS token before you know you need to provide some values for settings about the token, such as expiry date, scope, and service. It is no different here. The easiest way to do this in your ARM template is using either a Parameter (if you want people to be able to change this) or a variable. We are going to use a parameter.

The settings we need to provide are:

  • signedService - This indicates which storage service the token can access. The value can be b (for Blob), f (file), q (queue), t (table). You can specify multiples (bf etc.)
  • signedPermission - This indicates what permissions this token grants - r (read), w (write), d (delete), l (list), a (add), c (create), u (update), p (process). Again you can specify multiples
  • signedExpiry - The date and time the token expires
  • signedResourceTypes - This specifies the scope of the token - s (service), c (container), o (object)

These are all the mandatory values, and you can also specify the following optional ones:

  • signedIP - The IP address or range that can use this token
  • signedStart - The start date and time for the token
  • signedProtocol - Whether it supports https and/or http
  • keyToSign - The key to sign the account SAS token with

In our example, we want to default to creating a SAS token that can download a blob, until the 1st of March 2019, but allow this to be overwritten at run time. So we create a parameter in the template with default values as follows:

   "accountSasProperties": {
            "type": "object",
            "defaultValue": {
                "signedServices": "b",
                "signedPermission": "rw",
                "signedExpiry": "2019-03-01T00:00:01Z",
                "signedResourceTypes": "o"
            }
        }
        

Secret Creation

Now that we have our properties defined, the rest is simple. We are just going to create a secret and set the value to the SAS token. We get the value using the "listAccountSas" function. This function has 3 parameters:

  • The name of the storage account to generate the token for
  • The API version to use
  • Our SAS token properties object

So our value looks like this:

"value": "[listAccountSas(parameters('StorageAccountName'), '2018-07-01', parameters('accountSasProperties')).accountSasToken]"

Note that listAccountSas returns a key object, so we append the "accountSasToken" parameter to get the token as a string.

Our full secret object looks like this:

 {
            "apiVersion": "2018-02-14",
            "type": "Microsoft.KeyVault/vaults/secrets",
            "dependsOn": [
                "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]"
            ],
            "name": "[concat(parameters('keyVaultName'), '/', 'StorageSaSToken')]",
            "properties": {
                "value": "[listAccountSas(parameters('StorageAccountName'), '2018-07-01', parameters('accountSasProperties')).accountSasToken]"
            }
        }

Once we run our template, we can check the newly created Key Vault and see a secret with our SAS token as the value.

Key vault Secret

The full example ARM template is listed below and on GitHub here.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "keyVaultName": {
            "type": "string",
            "metadata": {
                "description": "Name of KeyVault to Store SaS Token"
            }
        },
        "tenantID": {
            "type": "string",
            "metadata": {
                "description": "Azure AD Tenant ID"
            }
        },
        "keyVaultAccessObjectID": {
            "type": "string",
            "metadata": {
                "description": "ID of user or App to grant access to KV"
            }
        },
        "StorageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Name of Storage Account to Create"
            }
        },
        "accountSasProperties": {
            "type": "object",
            "defaultValue": {
                "signedServices": "b",
                "signedPermission": "rw",
                "signedExpiry": "2019-03-01T00:00:01Z",
                "signedResourceTypes": "o"
            }
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2018-07-01",
            "name": "[parameters('StorageAccountName')]",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "[parameters('StorageAccountName')]"
            },
            "sku": {
                "name": "Standard_LRS"
            },
            "kind": "StorageV2"
        },
        {
            "type": "Microsoft.KeyVault/vaults",
            "apiVersion": "2018-02-14",
            "name": "[parameters('keyVaultName')]",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "[parameters('keyVaultName')]"
            },
            "properties": {
                "enabledForDeployment": true,
                "enabledForTemplateDeployment": true,
                "enabledForDiskEncryption": true,
                "tenantId": "[parameters('tenantID')]",
                "accessPolicies": [
                    {
                        "tenantId": "[parameters('tenantID')]",
                        "objectId": "[parameters('keyVaultAccessObjectID')]",
                        "permissions": {
                            "keys": [
                                "get"
                            ],
                            "secrets": [
                                "list",
                                "get",
                                "set"
                            ]
                        }
                    }
                ],
                "sku": {
                    "name": "standard",
                    "family": "A"
                }
            }
        },
        {
            "apiVersion": "2018-02-14",
            "type": "Microsoft.KeyVault/vaults/secrets",
            "dependsOn": [
                "[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]"
            ],
            "name": "[concat(parameters('keyVaultName'), '/', 'StorageSaSToken')]",
            "properties": {
                "value": "[listAccountSas(parameters('StorageAccountName'), '2018-07-01', parameters('accountSasProperties')).accountSasToken]"
            }
        }
    ],
    "outputs": {}
}