Azure Security

Improve storage account security with Azure RBAC and Terraform

Improve storage account security with Azure RBAC and Terraform

TL;DR

Terraform supports using Entra ID authentication to Azure Storage Accounts, and you can easily enable it.

Jump to recipe

Shared Access Keys

Authentication to Azure Storage Accounts is done by shared access keys out-of-the-box. Your user has a permission granting Microsoft.Storage/storageAccounts/listKeys/action, which in turn grants you access to list the shared access keys of storage accounts. Some magic happens in the portal, and you are able to access the data inside the storage account. This functionality is enabled by default, but is not in itself a gaping security hole.

The primary issues with Shared Access Keys:

  • Too wide permissions if acquired (all access to Storage Account data)
  • No granuar control inside the Storage Account
  • Implicit access to data without dataActions permissions

Microsoft even recommends using Entra ID instead of shared access keys in the first table row.

Azure AD provides superior security and ease of use over Shared Key for authorizing requests to Blob storage.

Also here you can see the same recommendation.

Microsoft recommends using Azure Active Directory (Azure AD) to authorize requests against blob, queue, and table data if possible, rather than using the account keys (Shared Key authorization).

Roles with listKeys permission

Azure has a large set of builtin roles. These roles grant permissions to specific actions/dataActions/notActions on specific resources. Some are obviously giving you permission to read data inside storage accounts, some are not as obvious.

Obvious roles:

  • Storage Account Contributor
  • Contributor
  • Owner
  • Reader and Data Access
  • Storage Account Key Operator Service Role

Not so obvious roles:

  • Disk Snapshot Contributor
  • Virtual Machine Contributor
  • Log Analytics Contributor
  • Logic App Contributor
  • App Compliance Automation Administrator

Meaning if you grant e.g. Virtual Machine Contributor on a wide scope, the assigned security principal can (at least in theory) list the shared access keys on any storage account in the scope. Effectively granting full data access inside the storage account.

RBAC on storage accounts

The Entra ID authorization part that Microsoft mentiones in their recommendations mean converting to allow access by way of RBAC roles instead of authenticating with a shared access key. These authorization methods are possible to use in parallell and you don’t need to disable access keys unless required for security purposes!

You can see a table of supported storage account sub resources here.

When using standard authentication the shared access key is passed in a connection string for authentication by the connection. This does not confirm a user identity, as anyone with the access keys kan authenticate like this. In contrast when using Entra ID authentication, a security principal (user, group, or service principal/managed identity) requests an OAuth 2.0 token from Entra ID. This token is then sent with any subsequent request, thereby authenticating the connection. These tokens are short-lived, and provided by Entra ID.

Benefits include “just in time” authentication and confirmation of a security principal’s identity.

Backend authentication

Backend authentication is required for storing a state remotely. The state is available in an external storage space, in my case most likely Azure Storage Account, and access requires authentication. There is also some network firewall at play here, but for now I will assume public access and no private endpoints. Network security for your state file could potentially be a subject for future posts.

Before accessing your state Terraform, Terraform will list shared access keys on the relevant storage account. Then these will be used in subsequent connections for updating, reading, or locking the state.

Storage Account management

Storage Account management actions performed by Terraform uses shared access keys to authenticate connections. Therefore the security principal running Terraform needs the listKeys permission on any storage account that is managed by Terraform.

Before doing anything requiring authentication to a storage account, Terraform will list Shared Access Keys, and use these in any subsequent connections. This can be anything relating to data inside a storage account: creating a queue, updating some blob content, creating containers etc. Not supported on Files or Tables currently.

Suspicious events

Some larger companies have extensive SOCs (Security Operations Centrals), and they analyse a wealth of information to find suspicious events. The event that is created when Terraform lists the mentioned access keys can be considered a suspicious event.

Anytime these keys are listed, there is a chance someone with contributor access on the storage account is trying to access data inside it. They might not be explicitly allowed to view the data, but have access to it implicitly.

Best case it is someone just browsing the portal and landing on the wrong page. Worst case it is an attacker that has gained access to an account with unintentional permissions on sensitive data..

In real life this is a good way of thinking:

Never attribute to malice that which can be attributed to incompetence.

When it comes to security, it might be too naive? 🤷‍♂️

Potential Terraform issues with access key authentication

By default Terraform uses Shared Access keys to access the state backend. This is not an issue if one storage account equals one state. The spn will be granted access to list keys for the entire storage account, and uses this to authenticate. No troubles.

If you have more than one state in that storage account, you can get some potentially disastrous consequences. Imagine you clone the backend config of the first backend, and manage to forget updating key/container settings. The new Terraform spn will attempt to write state to file, but find a state there already. Since I have not tested this myself, I can only guess what will happen.

Your new environment might attempt a backend reconfiguration, after you confirm that this is wanted. Since the new terraform environment has write access to the existing state files, this can result in an inconsistent state! Of course there are safeguards in place, but worst case I assume you can screw this up somehow…

In this case it would be better to have granuar Entra ID permissions on different containers within the storage account. That way one service principal can never attempt to write to the other state, because it does not have access! Granular permissions is given in storage accounts the same way as other places in Azure. The scope in this case would be different containers in the storage account.

Imagine we create a storage account called saterraformstates001 to store our terraform states.

  • saterraformstates001/production for production state.
  • saterraformstates001/test for test state.
  • saterraformstates001/dev for development state.

The best practice here would be having three different security principals (preferrably managed identities), and granting them Storage Blob Data Owner on their respective container. This improves adherance to least privilege principle, and also is a step towards zero trust. Another bonus is creating an additional safeguard against misconfigurations, as test can never write to prod and vice versa.

If using shared access keys here, you are implicitly granting all spns access to all three states!

Configuration in Terraform

The configuration is different for backend authentication and storage account related management tasks. Both of these methods work well with OpenID Connect.

Backend authentication config

This snippet will ensure Terraform authenticates to backend storage account with Entra ID instead of shared access keys:

terraform {
  backend "azurerm" {
    storage_account_name = "abcd1234"
    container_name       = "tfstate"
    key                  = "prod.terraform.tfstate"
    use_azuread_auth     = true # This enables RBAC instead of access keys
    subscription_id      = "00000000-0000-0000-0000-000000000000"
    tenant_id            = "00000000-0000-0000-0000-000000000000"
    use_oidc             = true # If you want to use OpenID Connect
  }
}

Please note the following:

When using AzureAD for Authentication to Storage you also need to ensure the Storage Blob Data Owner role is assigned.

Storage Account management config

This is configured in the AzureRM provider like so:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

provider "azurerm" {
  features {}
  use_oidc             = true # If you want to use OpenID Connect
  subscription_id      = "00000000-0000-0000-0000-000000000000"
  tenant_id            = "00000000-0000-0000-0000-000000000000"
  storage_use_azuread  = true # This enables RBAC instead of access keys
}

Documentation here.

A couple of gotchas worth mentioning:

This requires that the User/Service Principal being used has the associated Storage roles: Storage Blob Data Owner, Storage Blob Data Contributor.

The Files & Table Storage API's do not support authenticating via AzureAD and will continue to use a SharedKey to access the API’s.

Disable shared access key authorization

Before you disable the shared access key authorization, you should consider compatibility with other azure tools and services. Also consider the potential issues with Terraform management.

Disable with Terraform:

resource "azurerm_storage_account" "example" {
  name                      = "storageaccountname"
  resource_group_name       = "rgname"
  location                  = "norwayeast"
  account_tier              = "Standard"
  account_replication_type  = "GRS"
  shared_access_key_enabled = false # This property disables shared access key authorization
}

Monitor with Azure policy

You can monitor your Azure environment compliance with Azure Policy. Storage account security is no exception, and there is a builtin policy to audit or prevent this setting.

In summary

  • Use least privilege and Entra ID authentication for Storage Account connections in Terraform.
  • Disable shared access keys on storage accounts (if not using files/tables) to reduce attack vectors.

I have explained the difference in authenticating to storage accounts with access keys and Entra ID granular assignments. Hopefully this is something you can use in your solutions, and it has given you some insight into different authentication methods.

Please comment if you see something missing/wrong!