Storage Account SAS Tokens, Access Keys, And Connection Strings In Azure Bicep

Storage account (Azure Storage) is one of the core services in Azure. It is widely used by customers as well as other Azure services behind the scenes. Storage account comprises four services: blob, file, queue, and table services.

When working with storage accounts, proper security measures should be used to keep data safe. Probably, the most important measure is to use relevant authentication and authorization.

There are multiple ways how to authenticate/authorize to a storage account, for example, shared access signature (SAS), managed identities (system- and user-assigned), service principals, connection strings.

In this post, we will explore how to work with SAS tokens, access keys, and connection strings within Azure Bicep templates.

Contents:

Overview

This post consists of three main sections: SAS Tokens, Access Keys, and Connection Strings.

In the first section, we cover shared access signature in detail, in particular, how SAS tokens work, what types of SAS are available in the storage account, provide considerations on which SAS type to use, and then discuss account SAS, service SAS and stored access policies along with code samples.

In the second section, we talk about storage account access keys and how to retrieve them using Bicep.

The third section focuses on connection strings, it builds on the knowledge from previous sections and provides examples of how to use both access keys and shared access signatures in a connection string as an authorization credential.

NOTE: In some cases, you might want to put the access key, SAS token, connection string into a Key Vault to keep it secure. Learn how to do that in Key Vault Secrets Management in With Azure Bicep post.

Shared Access Signature (SAS) Tokens For a Storage Account

In this section, we are going to discuss how to generate a shared access signature (SAS) token for a storage account using Azure Bicep.

How Shared Access Signature Works?

Let’s briefly discuss how SAS tokens work, this would help better understand the topic in general.

NOTE: I’m not very familiar with the implementation of shared access signature technology in the storage account and also I’m not a security expert, so I can be wrong. Here, just sharing my mental model of how things work based on the public docs. Please let me know if you find errors.

Next, we discuss what every involved party does and how SAS tokens get created and validated.

Token generation:

  1. Get the key - either account key or user delegation key.
  2. Create a string-to-sign in a certain format which contains information about permissions, validity range, scope, etc.
  3. Sign this string-to-sign using the key, this will result in a signature.
  4. Create a token which contains the information from the string-to-sign and also include the signature.

End user:

  1. Send a request and attach the token as a query string.

Storage account server side:

  1. Take information from the token and reconstruct the string-to-sign.
  2. Get the key - the same one which was used to sign the token.
  3. Sign the string-to-sign and obtain a signature.
  4. Compare the incoming and generated signatures, if they match then the information is valid and matches the signature.
  5. Perform authorization based on the information, for example, if the current operation is permitted or the resource is within the defined scope, etc.

It’s worth mentioning that both parties which generate and validate the token have access to the same key. However, the key itself is never shared with the end user who is actually using our token to access resources in the storage account.

For more details, please refer to constructing the signature string and HMAC.

Shared Access Signature Types

Note that there are three types of SAS tokens:

Both Account SAS and Service SAS are signed with a storage account key and can be easily generated within Azure Bicep using listAccountSas and listServiceSas functions. We don’t talk about user delegation SAS here since there is no simple way to generate them in Azure Bicep.

In the following sections, we will cover how to create Account and Service SAS tokens using Bicep and discuss configuration values that can be passed during shared access signature creation.

Which SAS Type To Use?

After familiarizing with multiple types of shared access signatures, the next question is which one to use in our particular case. The following is my list of considerations when choosing a SAS token type (but keep in mind that I’m not an expert on security nor storage accounts).

  1. User Delegation SAS - the documentation recommends this type of SAS since it uses AAD credentials instead of account key and, hence, provides better security. However, as of this writing, only blob storage is supported, it doesn’t work with stored access policies, and it cannot be easily created in Azure Bicep.
  2. Stored Access Policy SAS - you might want to consider using stored access policies if you are issuing multiple tokens which have similar purpose, this way you can control permissions and validity range even after tokens are already created. Also, tokens can be easily revoked by modifying or deleting the associated access policy.
  3. Service SAS provides more granularity when issuing tokens compared to account SAS. It also has some parameters which are specific to each service type.
  4. Account SAS allows granting permissions to multiple services at the same time, also, there are operations which service SAS doesn’t support but account SAS does, such as managing containers, queues, tables, etc. This SAS type has the broadest set of capabilities.

Account SAS Token

To generate an account SAS token in Azure Bicep, we will use the built-in listAccountSas function. Under the hood, this function invokes storage account’s ListAccountSas REST API endpoint.

Parameters Of listAccountSas Function

The link above contains the list of all parameters this endpoint accepts, so please refer to it for the comprehensive list of possible values. However, we will still include comments here for better understanding.

NOTE: The prefix “signed” in parameter names means that these values are included in the string which is being signed.

Optional parameters:

Required parameters signedServices, signedResourceTypes, and signedPermission define what actions a client will be allowed to perform on what resources using the SAS token. Please refer to the documentation to understand what combination of values you need to support your use case.

Parameter signedExpiry is required and specifies when the SAS token will stop working. Please note that this date and time is not 100% precise since servers can potentially have some small time skew.

Generating an Account SAS Token In Azure Bicep

A simple example of creating a SAS token using listAccountSas function is presented below. The token is then returned in outputs.

// ========== account-sas-token.bicep ==========

// Defining a storage account resource, alternatively, could just reference an existing one
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

// Specifying configuration for the SAS token; not all possible fields are included in this example
var sasConfig = {
  signedResourceTypes: 'sco'
  signedPermission: 'r'
  signedServices: 'b'
  signedExpiry: '2022-04-25T00:00:00Z'
  signedProtocol: 'https'
  keyToSign: 'key2'
}

// Use sasConfig to generate an Account SAS token
output sasToken string = storageAccount.listAccountSas(storageAccount.apiVersion, sasConfig).accountSasToken
// Alternative way to invoke this function by passing the resource ID
// output sasToken string = listAccountSas(storageAccount.id, storageAccount.apiVersion, sasConfig).accountSasToken

Service SAS Token

To generate a service SAS token in Azure Bicep, we will use listServiceSas function. In turn, this function invokes storage account’s ListServiceSas REST API endpoint.

As the name suggests, this type of shared access signature is scoped only to one of the services (i.e. blob, file, queue, table). Also, service SAS provides better granularity by allowing to specify which resource we grant permissions to.

NOTE: Some operations, for example, creating and deleting containers, queues, tables, cannot be performed using a service SAS. For this, you’ll need to create an account SAS.

Parameters Of listServiceSas Function

There are a lot of parameters that can be specified when creating a service SAS token, some parameters are specific to the service type in question. The comprehensive list of parameters is available here and here. We will highlight a few of them:

Granularity Of canonicalizedResource Path

As stated in the docs, canonicalizedResource is a canonical path to a signed resource. To understand what paths are valid, we need to understand what a resource is within each service.

The following are resources: container and blob in a blob service, file share and file in a file service, queue in a queue service, and table in a table service. Quite simple indeed.

A couple of important notes:

Generating a Service SAS Token In Azure Bicep

The following code snippet illustrates how to create a service SAS token using Azure Bicep. As you can see, it is very similar to generating an account SAS.

// ========== service-sas-token.bicep ==========

// Defining a storage account resource, alternatively, could just reference an existing one
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

// Specifying configuration for the SAS token; not all possible fields are included in this example
var sasConfig = {
  // canonicalizedResource: '/blob/${storageAccount.name}/mycontainer' // Entire container
  canonicalizedResource: '/blob/${storageAccount.name}/mycontainer/some/path/test.py' // Specific blob in the container
  signedResource: 'b'
  signedPermission: 'r'
  signedExpiry: '2022-05-01T00:00:00Z'
  signedProtocol: 'https'
  keyToSign: 'key2'
}

// Use sasConfig to generate a Service SAS token
output sasToken string = storageAccount.listServiceSas(storageAccount.apiVersion, sasConfig).serviceSasToken
// Alternatively, we can pass resource ID 
// output sasToken string = listServiceSas(storageAccount.id, storageAccount.apiVersion, sasConfig).serviceSasToken

Service SAS Token With a Stored Access Policy

Stored access policy can contain a list of permissions, start and end time. If shared access signature references a stored policy, then these properties are applied to the SAS token.

This allows to group related SAS with similar permissions and/or purpose. Additionally, changing the properties of the policy will change the properties of existing SAS tokens too.

Another advantage is that deleting a stored access policy allows to revoke all related SAS tokens without rotating the storage account key which was used to sign the token string.

NOTE: Stored access policies cannot be created or managed through Azure Bicep or ARM templates. Thus, in the following examples we are using stored access policy created before.

Generating a SAS Token With a Stored Access Policy

Creating this type of SAS is not very different from a regular service SAS. In this case we just need to pass slightly different parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ========== stored-access-policy-sas.bicep ==========

// Defining a storage account resource, alternatively, could just reference an existing resource
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

// Specifying configuration for the SAS token; not all possible fields are included in this example
var sasConfig = {
  // canonicalizedResource: '/blob/${storageAccount.name}/mycontainer' // Entire container
  canonicalizedResource: '/blob/${storageAccount.name}/mycontainer/folder1/folder2/test.py' // Specific blob in the container
  signedIdentifier: 'my-stored-policy-1'
  signedResource: 'b'
  signedProtocol: 'https'
  keyToSign: 'key2'
}

// Use sasConfig to generate a Service SAS token
output sasToken string = storageAccount.listServiceSas(storageAccount.apiVersion, sasConfig).serviceSasToken
// Alternatively, we can pass resource ID 
// output sasToken string = listServiceSas(storageAccount.id, storageAccount.apiVersion, sasConfig).serviceSasToken

Storage Account Access Keys

Access keys, or account keys, can be used as one of the ways to authorize to a storage account. Additionally, an access keys are used to encrypt SAS tokens. See the previous section for more details about SAS tokens.

NOTES:

Retrieving an Access Key For a Storage Account

When working with Azure Bicep, storage account access keys can be easily retrieved using listKeys function. The returned object contains both access keys for the storage account.

In the code snippet below, listKeys function is invoked on a storage account object, storageAccount here is a “symbolic name”. To learn more about symbolic names and how to create them, consider reading Reference New Or Existing Resource In Azure Bicep post.

// ========== storage-account-key.bicep ==========

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

output keysObj object = storageAccount.listKeys()
// The expression below is an alternative way to get the same result
// output keysObj object = listKeys(storageAccount.id, storageAccount.apiVersion)

// Returns the value of the first key
output key1 string = storageAccount.listKeys().keys[0].valuehe structure of the `keysObj` output is like the following:
{
  "keys": [
    {
      "creationTime": "2021-10-24T16:53:09.6150727Z",
      "keyName": "key1",
      "value": "71xqI98..",
      "permissions": "FULL"
    },
    {
      "creationTime": "2021-10-24T16:53:09.6150727Z",
      "keyName": "key2",
      "value": "w4mXsRV..",
      "permissions": "FULL"
    }
  ]
}

Get a Connection String For a Storage Account

A connection string is a string that contains necessary information to identify a storage account to connect to and also includes authorization credentials. Many applications use connection strings when working with storage accounts, one notable example is Azure App Service.

To create a connection string, we need to concatenate information in a certain format. There is a number of fields that can be included, but we’ll focus on the main ones to keep it simple. See details at Configure a connection string - Azure Storage.

Both an access key and a SAS token can be used as the authorization credential in the connection string. And we will discuss these two cases in the next two sections.

Connection String Using Account Key

The format of the connection string is shown below. At least, these three fields have to be specified, other optional fields can be added if needed.

DefaultEndpointsProtocol=[http|https];AccountName=<account_name>;AccountKey=<account_key>

Now, given the connection string format, we can create it in our Azure Bicep code.

// ========== connection-string-key.bicep ==========

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

// Taking the first key
var key = storageAccount.listKeys().keys[0].value

// Using string interpolation to create a connection string and return as output
output connectionStringKey string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${key}'

Connection String Using Shared Access Signature (SAS)

If we don’t want to grant full permissions to the user of the connection string, it is better to use shared access signature (SAS) for authorization rather than account keys.

The connection string format is a little bit different in this case compared when using an account key, below is the minimal example. Protocol field is not needed since it’s included in the SAS token.

BlobEndpoint=<blob_endpoint>;SharedAccessSignature=<sas_token>

The following code snippet illustrates how to create a connection string using a SAS token. Generating SAS tokens is discussed earlier in this post.

NOTES:

// ========== connection-string-sas.bicep ==========

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: 'stcontoso'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

// Specify desired permissions
var sasConfig = {
  signedExpiry: '2022-04-20T00:00:00.0000000Z'
  signedResourceTypes: 'sco'
  signedPermission: 'r'
  signedServices: 'b'
  signedProtocol: 'https'
}

// Alternatively, we could use listServiceSas function
var sasToken = storageAccount.listAccountSas(storageAccount.apiVersion, sasConfig).accountSasToken
// Connection string based on a SAS token
output connectionStringSAS string = 'BlobEndpoint=${storageAccount.properties.primaryEndpoints.blob};SharedAccessSignature=${sasToken}'