title: How to access Azure storage account from VM using Python and managed identity / SAS credentials
body: <p><strong>OBJECTIVE</strong> I have an Azure VM set up with a system assigned managed identity. I want to be able to:</p>
  
  <ol>
  <li><p>Allow users to access blobs inside storage account using the VM</p></li>
  <li><p>Ensure the users are not able to access the blobs external to the VM</p></li>
  <li><p>Use Python - most of our users are Python but not Powershell literate.</p></li>
  </ol>
  
  <p><strong>Setup details:</strong>
  <strong>Storage account:</strong> sa030802util. <strong>Container:</strong> testutils. <strong>Blob:</strong> hello3.txt
  <strong>Managed identity and roles.</strong> VM has a system assigned managed identity and contributor, storage account contributor, storage blob data contributor roles for sa030802util.</p>
  
  <p><strong>METHODS</strong>
  I have tried four methods to solve this problem. </p>
  
  <p><strong>Partially successful method 1: Python.</strong> In Python, I have been able to access the sa030802util storage account using the below code, derived from <a href="https://azure.microsoft.com/en-au/resources/samples/resource-manager-python-manage-resources-with-msi/" rel="nofollow noreferrer">link</a>, <a href="https://azure.microsoft.com/en-au/resources/samples/storage-python-manage/" rel="nofollow noreferrer">link</a> and <a href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-python" rel="nofollow noreferrer">link</a>. The problem is that this uses the storage account and keys rather than relying solely on the managed identity for the VM. My fear is that this leaves the possibility that users could extract the storage keys and gain access to the blobs outside the VM.</p>
  
  <p><strong>Pros:</strong> in Python. <strong>Con:</strong> not using managed identity to authenticate. BlockBlobService can't use MSI to authenticate (yet).</p>
  
  <p><strong>Partially successful method 2: Powershell.</strong> In Powershell, I have found two ways to access the blob using the managed identity. The challenge is that neither create a credential that I can easily substitute into Python as explained below. This first method is drawn from the Microsoft-taught Pluralsight course on Implementing Managed Identities for Microsoft Azure Resources (<a href="https://app.pluralsight.com/library/courses/microsoft-azure-resources-managed-identities-implementing/table-of-contents" rel="nofollow noreferrer">link</a>). It uses the Az module.</p>
  
  <p><strong>Pros:</strong> uses managed identity, relatively simple. <strong>Cons:</strong> not in Python. Does not generate a credential that could be used in Python.</p>
  
  <p><strong>Partially successful method 3: Powershell.</strong> This method is drawn from <a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/tutorial-windows-vm-access-storage-sas" rel="nofollow noreferrer">link</a>. It uses the VM managed identity to generate a SAS credential and access Azure Storage. </p>
  
  <p><strong>Pros:</strong> uses managed identity and generates SAS credential, which is potentially valuable as BlockBlobService in Python can accept a SAS token. <strong>Cons:</strong> not in Python. Overkill for Powershell itself given method 2 above achieves the same thing with less effort. I trialled it because I wanted to see if I could extract the SAS credential for use in Python.</p>
  
  <p><strong>Unsuccessful method 4: Python and Powershell.</strong> I thought I might be able to generate a SAS token in Powershell using method 3, then slot the token in to the BlockBlobService code from method 1. What I have isn't working. I suspect the reason is that the SAS credential was created for the testutils container, and the Python BlockBlobService needs a SAS credential for the sa030802util storage account.</p>
  
  <p><strong>Pro:</strong> would allow me to rely on the managed identity of the VM to access Azure Storage. <strong>Con:</strong> doesn't work!</p>
  
  <p><strong>QUESTIONS</strong>
  My questions are:</p>
  
  <ol>
  <li><p>Am I right in thinking it's better to rely on the VM managed identity and / or SAS credential than the account keys, if I want to make sure that users can only access the storage account inside the VM?</p></li>
  <li><p>Is there a way to cobble together code that lets me use Python to access the data? Is method 4 promising or a waste of time?</p></li>
  </ol>
  
  <p><strong>CODE</strong></p>
  
  <p><strong>Method 1: Python</strong></p>
  
  <pre><code>from azure.mgmt.storage import StorageManagementClient
  from azure.mgmt.storage.models import StorageAccountCreateParameters
  from msrestazure.azure_active_directory import MSIAuthentication
  from azure.mgmt.resource import SubscriptionClient
  from azure.storage.blob import BlockBlobService
  
  # find credentials and subscription id
  credentials = MSIAuthentication()
  subscription_client = SubscriptionClient(credentials)
  subscription = next(subscription_client.subscriptions.list())
  subscription_id = subscription.subscription_id
  
  # find storage keys
  storage_client = StorageManagementClient(credentials, subscription_id)
  storage_account = storage_client.storage_accounts.get_properties("&lt;resourcegroup&gt;", "sa030802util")
  storage_keys = storage_client.storage_accounts.list_keys("&lt;resourcegroup&gt;", "sa030802util")
  storage_keys = {v.key_name: v.value for v in storage_keys.keys}
  
  # create BlockBlobService and for e.g. print blobs in container
  account_name = "sa030802util"
  account_key = storage_keys["key1"]
  container_name = "testutils"
  block_blob_service = BlockBlobService(account_name = account_name, account_key = account_key)
  
  print("List blobs in container")
  generator = block_blob_service.list_blobs(container_name)
  for blob in generator:
      print("Blob name: " + blob.name)
  </code></pre>
  
  <p>The output of this code is:</p>
  
  <pre><code>List blobs in container
  Blob name: hello3.txt
  </code></pre>
  
  <p><strong>Method 2: Powershell</strong></p>
  
  <pre><code>Connect-AzAccount -MSI -Subscription &lt;subscriptionid&gt;
  $context = New-AzStorageContext -StorageAccountName sa030802util
  Get-AzStorageBlob -Name testutils -Context $context
  </code></pre>
  
  <p>The output of this code is:</p>
  
  <pre><code>Name                 BlobType  Length          ContentType                    LastModified         AccessTier SnapshotTime         IsDeleted
  ----                 --------  ------          -----------                    ------------         ---------- ------------         ---------
  hello3.txt           BlockBlob 15              application/octet-stream       2019-08-02 05:45:33Z Hot                             False
  </code></pre>
  
  <p><strong>Method 3: Powershell</strong></p>
  
  <pre><code># to get an access token using the VM's identity and use it to call Azure Resource Manager
  $response = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;resource=https%3A%2F%2Fmanagement.azure.com%2F' -Method GET -Headers @{Metadata="true"}
  $ content = $response.Content | ConvertFrom-Json
  #ArmToken = $content.access_token
  
  # to get SAS credential from Azure Resource Manager to make storage calls
  ## convert parameters to JSON
  $params = @{canonicalizedResource="/blob/sa030802util/testutils"; signedResource="c"; signedPermission="rcwl"; signedProtocol="https"; signedExpiry="2019-08-30T00:00:00Z"}
  $jsonParams = $params | ConvertTo-Json
  
  ## call storage listServiceSas endpoint to create SAS credential
  $sasResponse = Invoke-WebRequest -Uri https://management.azure.com/subscriptions/&lt;subscription_id&gt;/resourceGroups/&lt;resourceGroup&gt;/providers/Microsoft.Storage/storageAccounts/sa030802util/listServiceSas/?api-version=2018-02-01&amp;resource=https%3A%2F%2Fmanagement.azure.com%2F -Method POST -Body $jsonParams -Headers @{Authorization = "Bearer $ArmToken"} -UseBasicParsing
  
  ## extract SAS credential from response
  $sasContent = $sasResponse.Content | ConvertFrom-Json
  $sasCred = $sasContent.serviceSasToken
  
  # as example, list contents of container
  $context = New-AzStorageContext -StorageAccountName sa030802util -SasToken $sasCred
  Get-AzStorageBlob -Name testutils -Context $context
  </code></pre>
  
  <p>The output of this code is the same as for Method 2.</p>
  
  <p><strong>Method 4: Python and Powershell</strong>
  <strong>Powershell code</strong></p>
  
  <pre><code># to get an access token using the VM's identity and use it to call Azure Resource Manager
  $response = Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;resource=https%3A%2F%2Fmanagement.azure.com%2F' -Method GET -Headers @{Metadata="true"}
  $content = $response.Content | ConvertFrom-Json
  $ArmToken = $content.access_token
  
  # to get SAS credential from Azure Resource Manager to make storage calls
  ## convert parameters to JSON
  $params = @{canonicalizedResource="/blob/sa030802util/testutils"; signedResource="c"; signedPermission="rcwl"; signedProtocol="https"; signedExpiry="2019-08-30T00:00:00Z"}
  $jsonParams = $params | ConvertTo-Json
  
  ## call storage listServiceSas endpoint to create SAS credential
  $sasResponse = Invoke-WebRequest -Uri https://management.azure.com/subscriptions/&lt;subscription_id&gt;/resourceGroups/&lt;resourceGroup&gt;/providers/Microsoft.Storage/storageAccounts/sa030802util/listServiceSas/?api-version=2018-02-01&amp;resource=https%3A%2F%2Fmanagement.azure.com%2F -Method POST -Body $jsonParams -Headers @{Authorization = "Bearer $ArmToken"} -UseBasicParsing
  
  ## extract SAS credential from response
  $sasContent = $sasResponse.Content | ConvertFrom-Json
  $sasCred = $sasContent.serviceSasToken
  
  # then export the SAS credential ready to be used in Python
  </code></pre>
  
  <p><strong>Python code</strong></p>
  
  <pre><code>from azure.storage.blob import BlockBlobService, PublicAccess
  import os
  
  # import SAS credential
  with open("cred.txt") as f:
      line = f.readline()
  
  # create BlockBlobService
  block_blob_service = BlockBlobService(account_name = "sa030802util", sas_token=line)
  
  # print content of testutils container
  generator = block_blob_service.list_blobs("testutils")
  for blob in generator:
      print(blob.name)
  </code></pre>
  
  <p>The Python code returns the following error:</p>
  
  <pre><code>AzureHttpError: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. ErrorCode: AuthenticationFailed
  &lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;Error&gt;&lt;Code&gt;AuthenticationFailed&lt;/Code&gt;&lt;Message&gt;Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
  RequestId:&lt;subscriptionid&gt;
  Time:2019-08-05T05:33:40.0175771Z&lt;/Message&gt;&lt;AuthenticationErrorDetail&gt;Signature did not match. String to sign used was rcwl
  
  2019-08-30T00:00:00.0000000Z
  /blob/sa030802util/testutils
  
  
  https
  2018-03-28
  
  
  
  
  &lt;/AuthenticationErrorDetail&gt;&lt;/Error&gt;
  </code></pre>