ACloud.Solutions

Azure security

Azure storage account public access: how to find the one you forgot about

Someone needed to share a file with a supplier. The supplier's mail gateway rejected the attachment. So the container was made public for an afternoon, the link was sent, the supplier got the file, and everybody moved on. That was two years ago and the container is called exports.

Azure storage account public access is one of the few security findings where the impact is immediate and requires no attacker skill whatsoever. If anonymous read is enabled on a container, its contents are available to anyone who knows or guesses the URL, and blob URLs are guessable in the way that predictable things are.

Azure storage account public access has two switches, not one

The detail that makes this survive audits: there are two independent settings and both have to be permissive for anonymous access to work.

The account setting, allowBlobPublicAccess. This is the master switch. If it is false, no container in that account can be anonymously readable no matter what the container says.

The container setting, publicAccess. Per container, with three values. None means no anonymous access. Blob means anonymous read of blobs if you know the URL. Container means anonymous read plus the ability to list every blob in it, which is considerably worse because it removes the guessing.

The trap is checking only one. An account with allowBlobPublicAccess set to true is not necessarily exposing anything, because every container may be None. And a container set to Container is harmless while the account switch is false, right up until somebody flips the account switch for an unrelated reason and silently exposes it.

So a real audit reads both, and reports the combination. Microsoft's anonymous read access documentation covers the precedence and how to disable it at the account level.

Checking every subscription

Resource Graph finds the accounts, but the container setting is not in Resource Graph, so this is a two-stage job:

# Stage one: accounts where the master switch permits it.
Get-AzSubscription | ForEach-Object {
    $null = Set-AzContext -SubscriptionId $_.Id
    Get-AzStorageAccount | Where-Object { $_.AllowBlobPublicAccess -ne $false } |
        Select-Object @{n='Sub';e={$_.Context.Subscription}},
                      ResourceGroupName, StorageAccountName,
                      AllowBlobPublicAccess, EnableHttpsTrafficOnly
}

Note -ne $false rather than -eq $true. The property can be null on accounts created before the setting existed, and null behaves as permissive on older accounts. Checking for $true misses exactly the oldest accounts, which are the ones most likely to have been configured casually.

Then for each account that passes, enumerate containers and read PublicAccess. That needs a data-plane call rather than a management-plane one, which means the identity running the audit needs a data role such as Storage Blob Data Reader and not merely Reader. This surprises people: an account with Reader across the subscription can list storage accounts and cannot see whether their containers are public.

SAS tokens expiring in 2099

The other half of this, and the half nobody audits.

A shared access signature grants access without an identity. It is a URL with a signature in the query string, and anyone holding it has whatever it grants until it expires. Which means a SAS with a distant expiry is a permanent credential pasted into whatever channel it was shared through: an email, a Teams message, a ticket, a config file in a repository.

Expiry dates ten or twenty years out are common because the person creating one did not want to be called about it again, which is understandable and also creates a credential nobody can revoke without rotating the account key.

Two things worth knowing. Account-key-signed SAS tokens cannot be revoked individually; you rotate the key, which invalidates every SAS signed with it, including the ones something in production depends on. And stored access policies exist precisely to fix this, because a SAS tied to a policy can be revoked by changing the policy. Almost nobody uses them.

You cannot enumerate issued SAS tokens, which is the uncomfortable part. What you can do is check whether diagnostic logging records SAS-authenticated requests, and set a maximum SAS expiry at the account level so new ones cannot be created with absurd lifetimes.

The one that is public on purpose

Before switching anything off, work out which of these is deliberate, because some of them will be.

Static website hosting on a storage account uses a container called $web and it is supposed to be readable. Reporting it as a finding every month is how an audit loses credibility.

Public datasets, product downloads and documentation assets are all legitimate uses. So are container registries for public images and any container fronted by a CDN endpoint, where the origin being readable is the design.

The way to tell is not the setting, it is whether anything references the URL. Check the CDN profiles, check the DNS records, and check whether the container name looks like infrastructure or like somebody's afternoon. assets, static and $web are usually intentional. exports, temp, share and anything with a year in the name usually are not.

Then record the intentional ones somewhere the next audit will read, so the finding stops recurring. An exception with a reason and a date is a decision. An exception nobody wrote down is a finding you will rediscover quarterly and dismiss from memory, which is indistinguishable from not checking.

What to fix, in what order

Containers set to Container on an account that permits public access. Immediate. That combination allows listing, so the contents are enumerable rather than merely reachable. Find out what is in it before changing anything, because you need to know what was exposed.

Containers set to Blob on a permissive account. Same day. Reachable if the URL is known, and URLs leak through referrer headers, browser history and shared links.

Accounts permitting public access with no public containers. This week. Set allowBlobPublicAccess to false and the whole class of accident goes away. This is the highest-value change in the whole note because it is preventive rather than corrective.

SAS expiry policy. This month. Set a maximum, then work out what would break.

Do not simply switch things off before checking what uses them. A public container is sometimes deliberate: a website's static assets, a public dataset, a download somebody's product depends on. The finding is not "this is public", it is "is this public on purpose and does anybody know".

Where this sits

Public storage is one of the ten checks in the AzClean Toolkit, and it reports the account and container settings together rather than separately, for the reasons above.

It pairs with the NSG note because both findings come from the same habit of opening something temporarily, and with the RBAC note because the question "who could make this public again" is usually more interesting than the current setting.