backend/azurerm: support for authenticating via SAS Tokens (#19440)

* adding acceptance tests for msi auth

* including the resource group name in the tests

* backend/azurerm: support for authenticating using a SAS Token

* resolving merge conflicts

* moving the defer to prior to the error
This commit is contained in:
Tom Harvey 2018-11-22 18:02:33 +01:00 committed by GitHub
parent a0e5ebf3b3
commit 96b1c951fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 288 additions and 25 deletions

View File

@ -3,7 +3,10 @@ package azure
import (
"context"
"fmt"
"log"
"net/url"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
@ -25,6 +28,7 @@ type ArmClient struct {
environment azure.Environment
resourceGroupName string
storageAccountName string
sasToken string
}
func buildArmClient(config BackendConfig) (*ArmClient, error) {
@ -44,6 +48,12 @@ func buildArmClient(config BackendConfig) (*ArmClient, error) {
return &client, nil
}
// likewise with a SAS token
if config.SasToken != "" {
client.sasToken = config.SasToken
return &client, nil
}
builder := authentication.Builder{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
@ -85,6 +95,7 @@ func buildArmClient(config BackendConfig) (*ArmClient, error) {
func (c ArmClient) getBlobClient(ctx context.Context) (*storage.BlobStorageClient, error) {
if c.accessKey != "" {
log.Printf("Building the Blob Client from an Access Token")
storageClient, err := storage.NewBasicClientOnSovereignCloud(c.storageAccountName, c.accessKey, c.environment)
if err != nil {
return nil, fmt.Errorf("Error creating storage client for storage account %q: %s", c.storageAccountName, err)
@ -93,6 +104,20 @@ func (c ArmClient) getBlobClient(ctx context.Context) (*storage.BlobStorageClien
return &client, nil
}
if c.sasToken != "" {
log.Printf("Building the Blob Client from a SAS Token")
token := strings.TrimPrefix(c.sasToken, "?")
uri, err := url.ParseQuery(token)
if err != nil {
return nil, fmt.Errorf("Error parsing SAS Token: %+v", err)
}
storageClient := storage.NewAccountSASClient(c.storageAccountName, uri, c.environment)
client := storageClient.GetBlobService()
return &client, nil
}
log.Printf("Building the Blob Client from an Access Token (using user credentials)")
keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName)
if err != nil {
return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %s", c.storageAccountName, err)

View File

@ -44,6 +44,13 @@ func New() backend.Backend {
DefaultFunc: schema.EnvDefaultFunc("ARM_ACCESS_KEY", ""),
},
"sas_token": {
Type: schema.TypeString,
Optional: true,
Description: "A SAS Token used to interact with the Blob Storage Account.",
DefaultFunc: schema.EnvDefaultFunc("ARM_SAS_TOKEN", ""),
},
"resource_group_name": {
Type: schema.TypeString,
Optional: true,
@ -122,6 +129,7 @@ type BackendConfig struct {
Environment string
MsiEndpoint string
ResourceGroupName string
SasToken string
SubscriptionID string
TenantID string
UseMsi bool
@ -145,6 +153,7 @@ func (b *Backend) configure(ctx context.Context) error {
Environment: data.Get("environment").(string),
MsiEndpoint: data.Get("msi_endpoint").(string),
ResourceGroupName: data.Get("resource_group_name").(string),
SasToken: data.Get("sas_token").(string),
StorageAccountName: data.Get("storage_account_name").(string),
SubscriptionID: data.Get("arm_subscription_id").(string),
TenantID: data.Get("arm_tenant_id").(string),
@ -156,8 +165,8 @@ func (b *Backend) configure(ctx context.Context) error {
return err
}
if config.AccessKey == "" && config.ResourceGroupName == "" {
return fmt.Errorf("Either an Access Key or the Resource Group for the Storage Account must be specified")
if config.AccessKey == "" && config.SasToken == "" && config.ResourceGroupName == "" {
return fmt.Errorf("Either an Access Key / SAS Token or the Resource Group for the Storage Account must be specified")
}
b.armClient = armClient

View File

@ -43,17 +43,18 @@ func TestBackendAccessKeyBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
backend.TestBackendStates(t, b)
@ -67,11 +68,10 @@ func TestBackendManagedServiceIdentityBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
@ -87,6 +87,35 @@ func TestBackendManagedServiceIdentityBasic(t *testing.T) {
backend.TestBackendStates(t, b)
}
func TestBackendSASTokenBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
res := testResourceNames(rs, "testState")
armClient := buildTestClient(t, res)
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
t.Fatalf("Error creating Test Resources: %q", err)
}
sasToken, err := buildSasToken(res.storageAccountName, res.storageAccountAccessKey)
if err != nil {
t.Fatalf("Error building SAS Token: %+v", err)
}
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"sas_token": *sasToken,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
backend.TestBackendStates(t, b)
}
func TestBackendServicePrincipalBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
@ -95,11 +124,10 @@ func TestBackendServicePrincipalBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
@ -124,17 +152,17 @@ func TestBackendAccessKeyLocked(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
@ -142,6 +170,7 @@ func TestBackendAccessKeyLocked(t *testing.T) {
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
backend.TestBackendStateLocks(t, b1, b2)
@ -156,11 +185,10 @@ func TestBackendServicePrincipalLocked(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,

View File

@ -24,17 +24,17 @@ func TestRemoteClientAccessKeyBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
state, err := b.StateMgr(backend.DefaultStateName)
@ -53,11 +53,10 @@ func TestRemoteClientManagedServiceIdentityBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
@ -78,6 +77,40 @@ func TestRemoteClientManagedServiceIdentityBasic(t *testing.T) {
remote.TestClient(t, state.(*remote.State).Client)
}
func TestRemoteClientSasTokenBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
res := testResourceNames(rs, "testState")
armClient := buildTestClient(t, res)
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
t.Fatalf("Error creating Test Resources: %q", err)
}
sasToken, err := buildSasToken(res.storageAccountName, res.storageAccountAccessKey)
if err != nil {
t.Fatalf("Error building SAS Token: %+v", err)
}
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"sas_token": *sasToken,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
state, err := b.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
remote.TestClient(t, state.(*remote.State).Client)
}
func TestRemoteClientServicePrincipalBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
@ -86,11 +119,10 @@ func TestRemoteClientServicePrincipalBasic(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
@ -120,17 +152,17 @@ func TestRemoteClientAccessKeyLocks(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
@ -138,6 +170,7 @@ func TestRemoteClientAccessKeyLocks(t *testing.T) {
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"access_key": res.storageAccountAccessKey,
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)
s1, err := b1.StateMgr(backend.DefaultStateName)
@ -161,11 +194,10 @@ func TestRemoteClientServicePrincipalLocks(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
@ -212,11 +244,10 @@ func TestPutMaintainsMetaData(t *testing.T) {
ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
defer armClient.destroyTestResources(ctx, res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)
headerName := "acceptancetest"
expectedValue := "f3b56bad-33ad-4b93-a600-7a66e9cbd1eb"

View File

@ -7,10 +7,16 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
armStorage "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/storage/mgmt/storage"
"github.com/Azure/azure-sdk-for-go/storage"
sasStorage "github.com/hashicorp/go-azure-helpers/storage"
)
const (
sasSignedVersion = "2017-07-29"
)
// verify that we are doing ACC tests or the Azure tests specifically
@ -80,6 +86,31 @@ func buildTestClient(t *testing.T, res resourceNames) *ArmClient {
return armClient
}
func buildSasToken(accountName, accessKey string) (*string, error) {
// grant full access to Objects in the Blob Storage Account
permissions := "rwdlacup" // full control
resourceTypes := "sco" // service, container, object
services := "b" // blob
// Details on how to do this are here:
// https://docs.microsoft.com/en-us/rest/api/storageservices/Constructing-an-Account-SAS
signedProtocol := "https,http"
signedIp := ""
signedVersion := sasSignedVersion
utcNow := time.Now().UTC()
startDate := utcNow.Format(time.RFC3339)
endDate := utcNow.Add(time.Hour * 24).Format(time.RFC3339)
sasToken, err := sasStorage.ComputeSASToken(accountName, accessKey, permissions, services, resourceTypes,
startDate, endDate, signedProtocol, signedIp, signedVersion)
if err != nil {
return nil, fmt.Errorf("Error computing SAS Token: %+v", err)
}
log.Printf("SAS Token should be %q", sasToken)
return &sasToken, nil
}
type resourceNames struct {
resourceGroup string
location string

2
go.mod
View File

@ -55,7 +55,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway v1.5.1 // indirect
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-azure-helpers v0.0.0-20181120094008-dd1e326c8888
github.com/hashicorp/go-azure-helpers v0.0.0-20181122151743-c51a3103be3b
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
github.com/hashicorp/go-cleanhttp v0.5.0
github.com/hashicorp/go-getter v0.0.0-20180327010114-90bb99a48d86

2
go.sum
View File

@ -124,6 +124,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-azure-helpers v0.0.0-20181120094008-dd1e326c8888 h1:QdenIfqH8llhlVDlGQWVUhg+L8pDT9VteOlMstWRl+w=
github.com/hashicorp/go-azure-helpers v0.0.0-20181120094008-dd1e326c8888/go.mod h1:e+GPy2nvD+spqsdjUyw5tbo73rBbu955QBaV9GZoBEA=
github.com/hashicorp/go-azure-helpers v0.0.0-20181122151743-c51a3103be3b h1:AJjRaIeZiWlhYVf2skNYOjooaaIOcQzS94a3iSKnXKE=
github.com/hashicorp/go-azure-helpers v0.0.0-20181122151743-c51a3103be3b/go.mod h1:e+GPy2nvD+spqsdjUyw5tbo73rBbu955QBaV9GZoBEA=
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de h1:XDCSythtg8aWSRSO29uwhgh7b127fWr+m5SemqjSUL8=
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de/go.mod h1:xIwEieBHERyEvaeKF/TcHh1Hu+lxPM+n2vT1+g9I4m4=
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=

View File

@ -0,0 +1,98 @@
package storage
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
)
const (
connStringAccountKeyKey = "AccountKey"
connStringAccountNameKey = "AccountName"
)
// ComputeSASToken computes the SAS Token for a Storage Account based on the
// access key & given permissions
func ComputeSASToken(accountName string,
accountKey string,
permissions string,
services string,
resourceTypes string,
start string,
expiry string,
signedProtocol string,
signedIp string, // nolint: unparam
signedVersion string, // nolint: unparam
) (string, error) {
// UTF-8 by default...
stringToSign := accountName + "\n"
stringToSign += permissions + "\n"
stringToSign += services + "\n"
stringToSign += resourceTypes + "\n"
stringToSign += start + "\n"
stringToSign += expiry + "\n"
stringToSign += signedIp + "\n"
stringToSign += signedProtocol + "\n"
stringToSign += signedVersion + "\n"
binaryKey, err := base64.StdEncoding.DecodeString(accountKey)
if err != nil {
return "", err
}
hasher := hmac.New(sha256.New, binaryKey)
hasher.Write([]byte(stringToSign))
signature := hasher.Sum(nil)
// Trial and error to determine which fields the Azure portal
// URL encodes for a query string and which it does not.
sasToken := "?sv=" + url.QueryEscape(signedVersion)
sasToken += "&ss=" + url.QueryEscape(services)
sasToken += "&srt=" + url.QueryEscape(resourceTypes)
sasToken += "&sp=" + url.QueryEscape(permissions)
sasToken += "&se=" + (expiry)
sasToken += "&st=" + (start)
sasToken += "&spr=" + (signedProtocol)
// this is consistent with how the Azure portal builds these.
if len(signedIp) > 0 {
sasToken += "&sip=" + signedIp
}
sasToken += "&sig=" + url.QueryEscape(base64.StdEncoding.EncodeToString(signature))
return sasToken, nil
}
// ParseStorageAccountConnectionString parses the Connection String for a Storage Account
func ParseStorageAccountConnectionString(connString string) (map[string]string, error) {
// This connection string was for a real storage account which has been deleted
// so its safe to include here for reference to understand the format.
// DefaultEndpointsProtocol=https;AccountName=azurermtestsa0;AccountKey=2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==;EndpointSuffix=core.windows.net
validKeys := map[string]bool{"DefaultEndpointsProtocol": true, "BlobEndpoint": true,
"AccountName": true, "AccountKey": true, "EndpointSuffix": true}
// The k-v pairs are separated with semi-colons
tokens := strings.Split(connString, ";")
kvp := make(map[string]string)
for _, atoken := range tokens {
// The individual k-v are separated by an equals sign.
kv := strings.SplitN(atoken, "=", 2)
key := kv[0]
val := kv[1]
if _, present := validKeys[key]; !present {
return nil, fmt.Errorf("[ERROR] Unknown Key: %s", key)
}
kvp[key] = val
}
if _, present := kvp[connStringAccountKeyKey]; !present {
return nil, fmt.Errorf("[ERROR] Storage Account Key not found in connection string: %s", connString)
}
return kvp, nil
}

3
vendor/modules.txt vendored
View File

@ -302,8 +302,9 @@ github.com/hashicorp/consul/lib/freeport
github.com/hashicorp/consul/testutil/retry
# github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/errwrap
# github.com/hashicorp/go-azure-helpers v0.0.0-20181120094008-dd1e326c8888
# github.com/hashicorp/go-azure-helpers v0.0.0-20181122151743-c51a3103be3b
github.com/hashicorp/go-azure-helpers/authentication
github.com/hashicorp/go-azure-helpers/storage
# github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
github.com/hashicorp/go-checkpoint
# github.com/hashicorp/go-cleanhttp v0.5.0

View File

@ -58,6 +58,22 @@ terraform {
}
```
When authenticating using a SAS Token associated with the Storage Account:
```hcl
terraform {
backend "azurerm" {
storage_account_name = "abcd1234"
container_name = "tfstate"
key = "prod.terraform.tfstate"
# rather than defining this inline, the SAS Token can also be sourced
# from an Environment Variable - more information is available below.
sas_token = "abcdefghijklmnopqrstuvwxyz0123456789..."
}
}
```
-> **NOTE:** When using a Service Principal or an Access Key - we recommend using a [Partial Configuration](/docs/backends/config.html) for the credentials.
## Example Referencing
@ -106,6 +122,22 @@ data "terraform_remote_state" "foo" {
access_key = "abcdefghijklmnopqrstuvwxyz0123456789..."
}
}
When authenticating using a SAS Token associated with the Storage Account:
```hcl
data "terraform_remote_state" "foo" {
backend = "azurerm"
config = {
storage_account_name = "terraform123abc"
container_name = "terraform-state"
key = "prod.terraform.tfstate"
# rather than defining this inline, the SAS Token can also be sourced
# from an Environment Variable - more information is available below.
sas_token = "abcdefghijklmnopqrstuvwxyz0123456789..."
}
}
```
## Configuration variables
@ -134,6 +166,12 @@ When authenticating using the Managed Service Identity (MSI) - the following fie
---
When authenticating using a SAS Token associated with the Storage Account - the following fields are also supported:
* `sas_token` - (Optional) The SAS Token used to access the Blob Storage Account. This can also be sourced from the `ARM_SAS_TOKEN` environment variable.
---
When authenticating using the Storage Account's Access Key - the following fields are also supported:
* `access_key` - (Optional) The Access Key used to access the Blob Storage Account. This can also be sourced from the `ARM_ACCESS_KEY` environment variable.