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
- Shared Access Signature (SAS) Tokens For a Storage Account
- Storage Account Access Keys
- Get a Connection String For a Storage Account
- Related Posts
- Useful Links
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.
- Any SAS token is only valid as long as the signing key is valid. This means that if the account key is rotated, all SAS tokens issued using this key will stop working regardless of their expiry dates.
- Leading question mark
?
can be a source of errors and confusion because sometimes applications expect it being included at the beginning of a SAS token and sometimes not. So, keep in mind to double check this if you are seeing issues.
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:
- Get the key - either account key or user delegation key.
- Create a string-to-sign in a certain format which contains information about permissions, validity range, scope, etc.
- Sign this string-to-sign using the key, this will result in a signature.
- Create a token which contains the information from the string-to-sign and also include the signature.
End user:
- Send a request and attach the token as a query string.
Storage account server side:
- Take information from the token and reconstruct the string-to-sign.
- Get the key - the same one which was used to sign the token.
- Sign the string-to-sign and obtain a signature.
- Compare the incoming and generated signatures, if they match then the information is valid and matches the signature.
- 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:
- Account SAS - defined at the level of a storage account; can give access to multiple services (e.g. blob, queue, file, table) at the same time, supports some operations which are not supported by other SAS types.
- Service SAS - scoped to only one service; includes a path to the resource for which access is provided; can reference a stored access policy to specify SAS token permissions.
- User Delegation SAS - a different approach where signing is done with a user delegation key which is issued by AAD, and not with an account key; only blob service is supported as of this writing. There is no built-in support for this type of SAS tokens in Azure Bicep.
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).
- 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.
- 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.
- Service SAS provides more granularity when issuing tokens compared to account SAS. It also has some parameters which are specific to each service type.
- 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:
- keyToSign - typically, the value can be
key1
orkey2
; specifies which account key to use for signing, this might be useful to track in case you rotate your access keys. - signedProtocol - can be
http
orhttps,http
; only requests via this protocol(s) will be accepted. - signedStart - specifies when the token becomes valid, note that the time is not 100% precise since machines can have time skew; if not specified, then the token is valid right away.
- signedIp - a specific IP address or a range; might be useful to restrict if you know the IPs of the servers which will be using the generated SAS token.
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:
- keyToSign, signedProtocol, signedStart, and signedIp parameters are similar to account SAS described above.
- canonicalizedResource - the canonical path to the resource we grant permissions to. Note that the canonical path has a special format depending on the API version, read format details at the bottom of this section.
- signedIdentifier - an identifier of a stored access policy defined within the service; the stored access policy specifies permissions and the validity date range.
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:
- By default, one cannot grant permissions to a folder in a container of a blob service. Actually, a SAS token can be generated but it won’t pass validation when trying to access the resource. The reason is that there are no directories in a blob storage because it has a flat structure, and using slash
/
in the name is just a convenience. - If hierarchical namespace is enabled, then permissions can be granted on the level of a directory. This is because there is a hierarchical structure (i.e. folders) when using hierarchical namespace feature.
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:
- Parameter
signedIdentifier
must be provided (line 17), it is an identifier of the stored access policy to reference. - Parameters that are already specified in the stored policy should not be included in the request since it will create a conflict.
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:
- An access key can be rotated, this means that a new key is created and the older one becomes invalid. This can be done on a periodically or ad-hoc when the current key is compromised.
- Each storage account has two access keys, having two keys allows rotating them one by one without downtime for clients. So, keep in mind that you have two keys at your disposal.
- In general, it is a good idea to avoid using access keys for authorization when interacting with a storage account. The reason is that access key grants a lot of rights, this could be very harmful if it is compromised. Therefore, consider using other auth options like AAD or SAS tokens.
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:
- An Account SAS token is used in this example, however, Service SAS token can also be used.
- The user of the connection string will have permissions as specified in the SAS token.
- Blob endpoint URI is retrieved dynamically from the properties of the storage account.
// ========== 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}'
Related Posts
- Key Vault & Secrets Management With Azure Bicep - Create, Reference, Output Examples
- Using Key Vault Secrets As Secure Parameters In Azure Bicep - Template & Module Inputs
- Learn Modules In Azure Bicep - Basics To Advanced, How It Works, Nested Modules, Outputs, Scopes
- Reference New Or Existing Resource In Azure Bicep
- Parameters In Azure Bicep - Ultimate Guide With Examples
- Child Resources In Azure Bicep - 3 Ways To Declare, Loops, Conditions
- 5 Ways To Deploy Bicep File With Parameters - Azure DevOps, PowerShell, CLI, Portal, Cloud Shell
Useful Links
- Grant limited access to data with shared access signatures (SAS) - Azure Storage | Microsoft Docs
- Delegate access with a shared access signature - Azure Storage | Microsoft Docs
- List Account SAS - REST API (Azure Storage Resource Provider) | Microsoft Docs
- List Service SAS - REST API (Azure Storage Resource Provider) | Microsoft Docs
- List Keys - REST API (Azure Storage Resource Provider) | Microsoft Docs
- Configure a connection string - Azure Storage | Microsoft Docs