Back to blog
AzureBest PracticesCloud SecurityIdentity & AccessStorage

Azure Storage Queue Has Permissive Access: Locking Down Shared Key Auth

Learn why Azure Storage queues with shared key access over public networks are a critical risk, and how to disable shared keys and lock down access.

TL;DR

This check flags Azure Storage accounts where the queue service can be reached over shared key authentication from public networks. Shared keys grant full account access and never expire, so an exposed key is a full compromise. Disable shared key auth, restrict network access, and switch your queue clients to Azure AD identities.

Azure Storage queues are a workhorse for decoupling services. They sit quietly behind background workers, order pipelines, and event fan-out logic, which is exactly why their security posture tends to get overlooked. When a queue service still accepts shared key authentication and is reachable from the public internet, you have a combination that turns one leaked secret into a full account takeover.

The Lensix check storage_queueacl looks for this exact condition on Azure Storage accounts and tells you which accounts need attention.


What this check detects

The check inspects each Azure Storage account and evaluates two things together:

  • Shared key access is enabled. The account allows authorization using the account access keys (the long, static keys you see under Access keys in the portal).
  • Public network access is open. The account's network rules permit traffic from any network rather than being scoped to specific VNets, IP ranges, or private endpoints.

When both are true, the queue service endpoint (https://<account>.queue.core.windows.net) can be authenticated with a shared key by anyone who has that key, from anywhere. The check fails because that is a wide blast radius for a credential that grants god-mode over the storage account.

Note: A storage account access key authorizes every service in the account, including blobs, files, tables, and queues. There is no scoping to a single queue or a single permission. Two keys exist per account so you can rotate without downtime, but each one is all-powerful while active.


Why it matters

Shared keys are the riskiest credential type in Azure Storage, and the reasons stack up quickly.

The key is a single point of total failure

Account keys do not expire, cannot be scoped, and cannot be tied to an identity for audit purposes. If a key leaks into a Git commit, a CI log, a container image layer, or a misconfigured app setting, an attacker who finds it can read every message off your queues, inject poisoned messages, delete queues, and pivot to blobs and tables in the same account. Because all of this happens under one shared credential, your audit logs show storage activity but not who did it.

Queues carry trusted instructions

Queue messages are often treated as trusted input by the workers that consume them. If an attacker can write to a queue, they can drive downstream behavior: triggering jobs, replaying actions, or smuggling malicious payloads into a processing pipeline that never expected hostile input. Queue tampering is a quieter attack than a public blob leak, and it often goes unnoticed until something downstream breaks.

Public reachability removes the network safety net

If the account were locked to a private endpoint or a known IP range, a leaked key would still require the attacker to be on your network. With public access open, the network provides no defense at all. The key is the only thing standing between an attacker and your data.

Warning: Disabling shared key access affects more than the queue service. SAS tokens signed with the account key, classic AzCopy jobs, and older SDK clients all rely on shared key auth. Inventory what uses the account before you flip the switch, or you will break working pipelines.


How to fix it

There are two levers, and you should pull both: turn off shared key authorization and lock down public network access. The cleanest end state is queue clients authenticating with Azure AD (Microsoft Entra ID) over a private endpoint.

Step 1: Find out what currently uses the account key

Before disabling anything, check the SharedKeyAuthorization sign-in activity. Use the Storage account's Monitoring metrics or query the sign-in logs to confirm nothing legitimate still depends on the key.

az storage account show \
  --name mystorageacct \
  --resource-group my-rg \
  --query "{sharedKey: allowSharedKeyAccess, publicAccess: publicNetworkAccess, networkRules: networkAcls.defaultAction}"

Step 2: Move queue clients to Azure AD authentication

Grant your application's managed identity or service principal the right data-plane role, then update the client to use DefaultAzureCredential instead of a connection string.

# Assign the queue data role to a managed identity
az role assignment create \
  --assignee <managed-identity-object-id> \
  --role "Storage Queue Data Contributor" \
  --scope "/subscriptions/<sub>/resourceGroups/my-rg/providers/Microsoft.Storage/storageAccounts/mystorageacct"
// .NET client using Azure AD instead of a shared key
var client = new QueueClient(
    new Uri("https://mystorageacct.queue.core.windows.net/orders"),
    new DefaultAzureCredential());

Note: Use the data-plane roles, not the control-plane ones. Storage Queue Data Contributor allows reading and writing messages. The Contributor role on the account does not grant data access by default, and the Owner role can still read keys, which defeats the purpose.

Step 3: Disable shared key access

Danger: This command will immediately break any client, SAS token, or tool still authenticating with the account key. Confirm Step 1 shows no recent shared key sign-ins before running it in production.

az storage account update \
  --name mystorageacct \
  --resource-group my-rg \
  --allow-shared-key-access false

Step 4: Lock down public network access

Set the default network action to deny, then add back only the specific networks or private endpoints you trust.

# Deny by default
az storage account update \
  --name mystorageacct \
  --resource-group my-rg \
  --default-action Deny

# Allow a specific VNet subnet
az storage account network-rule add \
  --account-name mystorageacct \
  --resource-group my-rg \
  --vnet-name app-vnet \
  --subnet workers-subnet

For the strongest posture, create a private endpoint so queue traffic never traverses the public internet at all, and set --public-network-access Disabled.

Console steps

  1. Open the storage account in the Azure portal.
  2. Go to Settings > Configuration and set Allow storage account key access to Disabled.
  3. Go to Security + networking > Networking.
  4. Under Public network access, choose Enabled from selected virtual networks and IP addresses or Disabled.
  5. Add the VNets, IP ranges, or private endpoints your workloads use.
  6. Save and confirm your applications still connect using their Azure AD identities.

How to prevent it from happening again

Fixing one account is easy. Keeping every account compliant as your estate grows is the real work. Bake the controls into provisioning and policy so the bad state is impossible to create.

Enforce it in Terraform

Set the secure properties directly in your storage account resource so every account ships locked down.

resource "azurerm_storage_account" "this" {
  name                          = "mystorageacct"
  resource_group_name           = azurerm_resource_group.this.name
  location                      = azurerm_resource_group.this.location
  account_tier                  = "Standard"
  account_replication_type      = "LRS"

  shared_access_key_enabled     = false
  public_network_access_enabled = false

  network_rules {
    default_action = "Deny"
    virtual_network_subnet_ids = [azurerm_subnet.workers.id]
  }
}

Block it with Azure Policy

Use the built-in policies to deny or audit non-compliant accounts at the subscription or management group level:

  • Storage accounts should prevent shared key access
  • Storage accounts should disable public network access
  • Storage accounts should restrict network access

Assign them with a Deny effect so future deployments that try to enable shared keys are rejected before they reach the data plane.

Tip: Run the Lensix storage_queueacl check on a schedule and wire the result into your pull request pipeline. Catching a regression in a Terraform plan is far cheaper than catching it after a key has already been live in production for three weeks.

Gate it in CI/CD

Scan IaC before it merges. A simple policy check in your pipeline that fails when shared_access_key_enabled is true or default_action is Allow stops the misconfiguration at the source, where the fix is a one-line edit rather than an incident.


Best practices

  • Treat Azure AD as the default auth method. Shared keys should be the exception you justify, not the path of least resistance.
  • Rotate keys you cannot yet disable. If a workload still needs a key, rotate it on a schedule and store it in Key Vault, never in app settings or source control.
  • Prefer user delegation SAS over account SAS. When you do need a SAS, sign it with an Azure AD identity rather than the account key, so it inherits the identity's permissions and audit trail.
  • Use private endpoints for production data services. Public network access plus default-deny rules is good; no public surface at all is better.
  • Log and alert on shared key sign-ins. Even on accounts you intend to lock down, monitoring SharedKeyAuthorization events tells you when a forgotten client is still using a key.
  • Apply the same posture across blobs, files, and tables. The account key authorizes all of them, so securing the queue service without securing the rest leaves the door open.

The queue service rarely makes headlines, which is precisely why a permissive configuration can linger for months. A leaked account key on a publicly reachable queue is not a theoretical risk, it is a credential that grants complete control of your storage account to anyone who finds it. Disable shared keys, scope the network, and let identities do the authenticating.