Accessing Azure Storage Account from VM using System Assigned Identity & Roles

In this blog post, we will start exploring identities & Role-based access control (RBAC) in Azure for accessing different Azure resources from applications. We will create an infrastructure consisting Azure Storage Account which will be having 2 blob storage containers. We will also create one virtual machine which will be allowed as a Reader for that storage account and also a Contributor for one of the blob storage containers.

What is System Assigned Identity?

Azure will give some ID in Azure Active Directory to azure resources which are created by us. This kind of ID will get created at the time of resource created & gets destroyed at the time of resource destruction. only that Azure resources can use this identity to request tokens from Azure AD.

You can't share these IDs with other Azure resources. It is meant for only that resource.

All code used in this post is available at https://github.com/lets-learn-it/terraform-learning/tree/azure/07-system-assigned-identities

Plan of Action

  1. Creating resource group, vnet, & 1 public subnet.
  2. A virtual machine in public subnet with SSH access allowed & also system assigned identity.
  3. Storage account with 2 blob storage containers.
  4. Role assignments
  5. Checking access from the virtual machine

We will create 2 modules for the Virtual machine & Storage account. Below is my folder structure

Creating RG, Vnet & subnet

Resource Group

All resources will be placed in this single resource group.

resource "azurerm_resource_group" "example" {
  name     = var.resource_group_name
  location = "East US"
}

Vnet & public subnet

We need one public subnet for virtual machines & make sure to specify a service endpoint for storage.

resource "azurerm_virtual_network" "example" {
  name                = "example-network"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}

resource "azurerm_subnet" "public_subnet" {
  name                 = "public_subnet"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.0.1.0/24"]
  service_endpoints    = ["Microsoft.Storage"]
}

Creating Virtual Machine (Module)

Creating Public IP

We need public IP so that we can SSH into this machine.

resource "azurerm_public_ip" "public_ip" {
  name                = format("%s_%s", var.name, "ip")
  resource_group_name = var.resource_group_name
  location            = var.location
  allocation_method   = "Dynamic"
}

Network Interface

resource "azurerm_network_interface" "example" {
  name                = format("%s_%s", var.name, "network_interface")
  location            = var.location
  resource_group_name = var.resource_group_name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = var.subnet_id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.public_ip.id
  }
}

Network Security Group

While creating NSG, make sure to open port 22 for protocol TCP (for SSH access). Associate network interface (previously created) with NSG.

resource "azurerm_network_security_group" "nsg" {
  name                = format("%s_%s", var.name, "nsg")
  location            = var.location
  resource_group_name = var.resource_group_name

  security_rule {
    name                       = "allow_ssh_sg"
    priority                   = 100 
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  depends_on = [
    azurerm_network_interface.example
  ]
}

resource "azurerm_network_interface_security_group_association" "association" {
  network_interface_id      = azurerm_network_interface.example.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

Virtual Machine

Now create a virtual machine with system assigned identity

resource "azurerm_linux_virtual_machine" "example" {
  name                = format("%s%s", var.name, "vm")
  resource_group_name = var.resource_group_name
  location            = var.location
  size                = "Standard_B1s"
  admin_username      = "adminuser"

  network_interface_ids = [
    azurerm_network_interface.example.id,
  ]

  admin_ssh_key {
    username   = "adminuser"
    public_key = file(var.public_key_path)
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }

  identity {
    type = "SystemAssigned"
  }
}

We need to output some variables which are required while creating infrastructure like principal_id for role assignment.

output "public_ip" {
  value = azurerm_public_ip.public_ip.ip_address
}

output "vm_id" {
  value = azurerm_linux_virtual_machine.example.id
}

output "vm_pricipal_id" {
  value = azurerm_linux_virtual_machine.example.identity[0].principal_id
}

Now, use the above module & create a virtual machine but make sure to declare all variables used in that module.

module "vm" {
    source = "./vm/"
    
    resource_group_name = azurerm_resource_group.example.name
    location            = azurerm_resource_group.example.location
    public_key_path     = "<public_key_path>"
    name                = "demo"
    subnet_id           = azurerm_subnet.public_subnet.id
}

Creating Storage Account

Module for the storage account

resource "azurerm_storage_account" "storage" {
  name                     = format("%s%s", var.name, "storage9553")
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  network_rules {
      default_action = "Deny"
      ip_rules = var.white_list_ip
      virtual_network_subnet_ids = var.whitelist_subnet_ids
  }

  tags = {
    environment = "staging"
  }
}

Let's output some variables like storage_account_id

output "storage_account_id" {
    value = azurerm_storage_account.storage.id
}

Using storage account module & create infrastructure & also create 2 blob storage containers. Make sure to add your IP to the list white_list_ip else terraform unable t create containers.

module "storage_account" {
    source = "./storageaccount"

    resource_group_name = azurerm_resource_group.example.name
    location            = azurerm_resource_group.example.location
    name                = "demo12"
    # whitelist ip of machine from which terraform creating infra
    # else terraform apply will fail with 403
    white_list_ip        = ["106.210.242.214"]
    whitelist_subnet_ids = [azurerm_subnet.public_subnet.id]
}

resource "azurerm_storage_container" "container" {
  name                  = "demo"
  storage_account_name  = module.storage_account.storage_account_name
  container_access_type = "private"
  depends_on = [
    module.storage_account
  ]
}

resource "azurerm_storage_container" "container2" {
  name                  = "demo1"
  storage_account_name  = module.storage_account.storage_account_name
  container_access_type = "private"
  depends_on = [
    module.storage_account
  ]
}

Role Assignments

We will assign Reader role to storage account & Storage Blob Data Contributor role to demo1 container. This means we can list containers in the storage account but are only able to write in demo1 container. The above 2 roles are already defined by Azure so no need to create them.


# Read role for storage account
resource "azurerm_role_assignment" "storage" {
    scope = module.storage_account.storage_account_id

    # using azure defined role
    role_definition_name = "Reader"

    principal_id = module.vm.vm_pricipal_id
}

# Write role for container
resource "azurerm_role_assignment" "container" {
    scope = azurerm_storage_container.container2.resource_manager_id

    # using azure defined role
    role_definition_name = "Storage Blob Data Contributor"

    principal_id = module.vm.vm_pricipal_id
}

When you apply the above terraform code, it will create 13 resources.

Checking Access from VM

Connect to the virtual machine using SSH

ssh -i <pvt_key> adminuser@<vm_ip>

Install az CLI in that machine. It depends on your machine's OS. If you used the same OS as I used then check https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt

Login

# login using identity
az login --identity

List containers

# list containers
# make sure that --auth-mode is login
az storage container list --account-name demo12storage9553 --auth-mode login

Upload File in demo1

First create one file.

echo "Hello World" > hello.txt

Now, upload hello.txt to demo1

# upload to demo2.
az storage blob upload --account-name demo12storage9553 --container-name demo1 --name hello-world.txt --file hello.txt --auth-mode login

Upload file in demo

VM doesn't have write access to demo the container. Let's try to upload file in demo

The End

Make sure to destroy all created resources.

Parikshit Patil

Parikshit Patil

Currently working as Software Engineer at Siemens Industry Software Pvt. Ltd. Certified AWS Certified Sysops Administrator - Associate.
Kavathe-Ekand, MH India