Contents

How to create SAS (Shared Access Signature) and upload Azure Blob through Azure Storage REST API and native Ansible

This post shows how to use Shared Access Signature Authentication in Ansible using the native REST API, but the concept utilized here can be applied to any language and/or platform. The same SAS procedure/script can be used for any Azure Storage API integration like Tables and Queues.

Azure Blob creation using native Ansible on Azure Storage REST API

Once, in a very restricted environment with no dependencies allowed, I had to generate an Azure blob from the client side. I couldn’t use Ansible modules like azure_rm_storageblob or Azure utils like azcopy, thus the Azure Blob Service REST API was my only option.

Shared Access Signature x Azure Storage REST API

Because it requires an SAS (Shared Access Signature) to make the permission, that REST API has some quirks.

The Azure Storage Key is used to construct an SAS (Shared Access Signature). We need to construct an Authorization header with the Storage Account name and Signature as seen below, however it’s important to note that the Signature must be recreated for each request. We can’t specify a fixed value for it.

1
Authorization="SharedKey <storage account name>:<signature>"  

Signature token is a unique hash-based key which is created for every REST Api URI you access. Which means we need a key created on the fly during each URI call.

Azure Storage REST API Headers

We need to set values in the API Header call like doc here suggests:

1
2
3
4
5
6
7
Request Headers:  
x-ms-version: 2015-02-21  
x-ms-date: <date>  
Content-Type: text/plain; charset=UTF-8  
x-ms-blob-type: BlockBlob  
Authorization: SharedKey myaccount:YhuFJjN4fAR8/AmBrqBz7MG2uFinQ4rkh4dscbj598g=  
Content-Length: 11 

Content-lenght and x-ms-date values vary per request and both values must be xactly the same between SAS creation and REST Api Blob creation. Plus we have to correctly define the CanonicalizedHeaders and CanonicalizedResource, using below values that will work fine for defined Azure API version (2015-02-21).

Here what Wikipedia says about CanonicalizedHeaders and CanonicalizedResource:

Info
In computer science, canonicalization (sometimes standardization or normalization) is a process for converting data that has more than one possible representation into a “standard”, “normal”, or canonical form. In normal-speak, this means to take the list of items (such as headers in the case of Canonicalized Headers) and standardize them into a required format. Basically, Microsoft decided on a format and you need to match it.

Python SAS creation

We used Python to generate the Shared Access Signature and return the key to the Ansible before REST API call. That returns the SAS and Request time (time must be the same in the REST API call).

Code is below:

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import requests
import datetime
import hmac
import hashlib
import base64
import sys
storage_account_key = ''
container_name='test'
api_version = '2015-02-21'
request_time = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
storage_account_ name = sys.argv[1]
container_name = sys.argv[2]
blob_name = sys.argv[3]
content_length = sys.argv[4]
string_params = {
    'verb': 'PUT',
    'Content-Encoding': '',
    'Content-Language': '',
    'Content-Length': content_length,
    'Content-MD5': '',
    'Content-Type': 'text/plain; charset=UTF-8',
    'Date': '',
    'If-Modified-Since': '',
    'If-Match': '',
    'If-None-Match': '',
    'If-Unmodified-Since': '',
    'Range': '',
    'CanonicalizedHeaders': 'x-ms-blob-type:BlockBlob' + '\nx-ms-date:' + request_time + '\nx-ms-version:' + api_version + '\n',
    'CanonicalizedResource': '/' + storage_account_name +'/'+container_name+ '/' + blob_name
}

string_to_sign = (string_params['verb'] + '\n' 
                  + string_params['Content-Encoding'] + '\n'
                  + string_params['Content-Language'] + '\n'
                  + string_params['Content-Length'] + '\n'
                  + string_params['Content-MD5'] + '\n' 
                  + string_params['Content-Type'] + '\n' 
                  + string_params['Date'] + '\n' 
                  + string_params['If-Modified-Since'] + '\n'
                  + string_params['If-Match'] + '\n'
                  + string_params['If-None-Match'] + '\n'
                  + string_params['If-Unmodified-Since'] + '\n'
                  + string_params['Range'] + '\n'
                  + string_params['CanonicalizedHeaders']
                  + string_params['CanonicalizedResource'])

signed_string = base64.b64encode(hmac.new(base64.b64decode(storage_account_key), msg=string_to_sign.encode('utf-8'), digestmod=hashlib.sha256).digest()).decode()

print (signed_string+";"+request_time)

Strict parameterization

Values for CanonicalizedHeaders and CanonicalizedResource must be set correctly with new line characters. The values in string_params must match those in the Blob Creation Api request. All header parameters must also be appended to string_to_sign Anything that isn’t expected will cause the REST Api authentication to fail, with a return error like this:

CanonicalizedHeaders and CanonicalizedResource values must be correctly set with proper new lines characters. In string_params values must be the same of the Blob Creation Api call. As well as all header params must be appended to string_to_sign. Anything out of the expected is going to fail the REST Api authentication with return error similar to the below:

1
2
3
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:cbf12c65-c01e-00fc-1069-3a41a7000000
Time:2020-06-04T12:11:03.4295368Z</Message><AuthenticationErrorDetail>The MAC signature found in the HTTP request 'c9n6EKq9p6skUs17qGv/bW0yGRGjMzMrP7bgDwjRABg=' is not the same as any computed signature. Server used following string to sign: 'PUT

Any Azure Storage API, such as Tables and Queues, can be used with the same SAS procedure/script.  I’m hoping to save others time who might be getting errors like ‘The MAC signature discovered in the HTTP request is not the same as any computed signature.’

Ansible playbook for Blob Rest API PUT

Below the Ansible Code which makes the Blob creation. Same Header values are required. Blob content should be set in the Request Body.

 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
27
28
29
30
31
32
33
34
35
36
37
---
- name: Playbook to PUT blob content
  hosts: localhost
  gather_facts: no
  connection: local
  tasks:

  - set_fact:
       storage_account_name: marciotestblob
       blob_container_name: test
       blob_name: myfile.txt
       blob_content: Hello World Blob content

  - name: Generate Shared Access Signature
    shell: python  scripts/generate_key.py {{ storage_account_name }} {{ blob_container_name }} {{ blob_name }} {{ blob_content|length }}
    register: generate_key
    delegate_to: localhost

  - set_fact:
       sas_token: "{{ generate_key.stdout.split(';')[0] }}"
       request_time: "{{ generate_key.stdout.split(';')[1] }}"

  - name: Azure Blob REST API creation
    uri: 
      url: "https://{{ storage_account_name }}.blob.core.windows.net/{{ blob_container_name }}/{{ blob_name }}"
      headers: 
        x-ms-blob-type: BlockBlob
        x-ms-date: "{{ request_time }}"
        x-ms-version: "2015-02-21"
        Content-Type: "text/plain; charset=UTF-8"
        Content-Length: "{{ blob_content|length }}"
        Authorization: "SharedKey {{ storage_account_name }}:{{ sas_token }}"
      method: PUT
      body: "{{ blob_content }}"
      status_code: 201
      validate_certs: false
    delegate_to: localhost

In the Ansible playbook above, The file name and content of a blob file are hardcoded, but you can construct dynamic inputs based on your needs.

Gitlab repository with both scripts can be found at mvitor’s github.