AzureSeptember 22, 20267 min read

Azure Security Audit Checklist: What to Check in Your Subscription

Most Azure security problems are not clever attacks. They are defaults nobody went back to look at, and a couple of rules that quietly never fire.

Nobody hands you a security checklist when you inherit an Azure subscription. You get the keys, and the assumption that you already know what is in there.

So here is the list I would actually run, in the order I would run it. Every item is something you can check yourself in a few minutes, and the quotes below come from Microsoft's own documentation rather than from a vendor trying to sell you a scanner.

I have put the two that surprise people most at the top, because if you only do two things, do those.

1. Who can read your storage without you ever giving them access to it

Azure has two separate ways into blob data. One is the data roles you assign on purpose, like Storage Blob Data Reader. The other is the account access keys, and those are a different door with a different lock.

Any role that includes the permission "Microsoft.Storage/storageAccounts/listkeys/action" can fetch those keys. Microsoft spells out what that means: "By using this permission, a user can use the account access keys to access all data in a storage account."

All data. Not the container you meant to share. All of it.

The built-in roles carrying that permission are Owner, Contributor, and Storage Account Contributor. Contributor is the role most people hand out when somebody needs to "just deploy things". It does not appear on any data-access screen, and the person holding it never has to ask you for anything.

So the check is not "who has a storage data role". The check is who has Contributor or above on the subscription or on that resource group. Open Access control (IAM) on the subscription, look at the role assignments, and count how many people and service principals sit at Contributor or Owner. For most small teams that number is larger than it should be, and it is usually larger than anybody remembers.

The fix is not dramatic. Move the people who only need data to a data role, and keep Contributor for the ones who genuinely deploy infrastructure.

2. The one container that is public no matter what you set

Storage accounts have a master switch called Allow Blob anonymous access. Turn it off and, as Microsoft puts it, "any future anonymous requests to that account fail". That is the setting everybody reaches for, and it is the right one.

There is an exception written into the same page, and it is one sentence long: "Disallowing anonymous access for a storage account doesn't affect any static websites hosted in that storage account. The $web container is always publicly accessible."

Always. If you have ever switched on static website hosting for a storage account, even to try it, that account has a container called $web that the internet can read, and the account-level switch does not touch it.

That is correct behaviour, because a website nobody can read is not a website. It is worth knowing anyway, because people put things in $web that were never meant to be the website. Go and look at what is in yours.

3. Anonymous access is two switches, not one

Microsoft's position is clear: "Anonymous access to your data is always prohibited by default." For a container to be readable by the public, two separate things have to be true. The account has to allow anonymous access, and the container itself has to be set to Blob or Container rather than Private.

That is good design, because one accident is not enough to expose anything. It also means checking one of them tells you nothing. A storage account with the switch on and every container private is fine today and one click away from not being fine.

Check the account with the CLI: "az storage account show --name <account> --query allowBlobPublicAccess". Then check the containers with PowerShell: "Get-AzStorageContainer -Context $ctx | Select Name, PublicAccess". Anything that comes back as Blob or Container is readable by anyone who guesses the URL.

4. The deny rule that never fires

This is the one that catches people who are already being careful.

Network security group rules have a priority number, and Microsoft describes the order plainly: "Rules are processed in priority order, with lower numbers processed before higher numbers because lower numbers have higher priority. Once traffic matches a rule, processing stops."

Read that last sentence again. Processing stops. Not "the strictest rule wins", not "a deny beats an allow". The first rule that matches is the only rule that runs.

So if somebody opened port 22 to the world at priority 100 last year, and you added a rule denying 22 at priority 200 last week, your deny rule has never once fired. It sits underneath the allow, doing nothing, looking like protection on the screen.

When you audit an NSG, do not read the rules as a list of intentions. Read them in priority order, top down, and stop at the first one that matches the traffic you care about. That is exactly what Azure does.

5. What is actually open inbound

Every NSG comes with default rules you cannot delete. The last inbound one is DenyAllInbound at priority 65500, which blocks everything from 0.0.0.0/0 that nothing above it allowed. That is a sane floor.

The catch is in how you override it. Microsoft: "You can't remove the default rules, but you can override them by creating rules with higher priorities." Every custom rule you have ever added sits above that deny, because every custom rule has a lower number than 65500.

So go through each NSG and list the inbound rules whose source is Any or 0.0.0.0/0. For each one, ask whether it needs to be reachable from the entire internet or from a handful of addresses. Management ports are the ones to be hardest on. Port 22 and port 3389 open to Any is somebody else's Tuesday afternoon.

Ports 80 and 443 open to the world are usually fine, because that is what a web server is for. The question is never "is this port open", it is "should this particular thing be reachable by everyone".

6. Outbound is wide open, and that is the default

Most people audit inbound and stop. Look at the default outbound rules and you will find AllowInternetOutBound sitting at priority 65001, permitting any source to reach the internet on any port.

That means every machine in your subscription can talk to anything, anywhere, unless you wrote a rule saying otherwise. It is convenient and it is why installing things works without you configuring anything.

It also means that if something on a machine gets compromised, nothing in the network stops it phoning out. You do not have to lock this down today. You should know it is the state you are in, because most people assume the opposite.

7. MFA on the accounts that can do anything

Go back to that Access control (IAM) list from the first check. For every human sitting at Owner or Contributor, confirm multi-factor authentication is actually on for their account.

One password with Owner on the subscription is the whole subscription. Every other item on this list is about limiting what a mistake can reach. This one is about whether somebody has to make a mistake at all.

While you are there, look for accounts belonging to people who have left, and service principals created for a project that finished. Those do not announce themselves. You have to go looking.

8. One dated thing worth knowing

If you rely on NSG flow logs to see what is talking to what, they retire on 30 September 2027, and Microsoft has already stopped allowing new ones to be created. The replacement is virtual network flow logs.

After that date Azure deletes the existing flow log resources in your subscription. The records already written to storage stay and keep following their retention policy, so nothing you have collected disappears. The collection just stops.

That is a long way off. It is on the list because it is the kind of thing that breaks quietly, on a date nobody put in a calendar.

Doing this by hand

Run the whole list once. It takes an afternoon on a small subscription and it is genuinely worth the afternoon, because you come out of it knowing where your own edges are.

The problem is the second time. Nothing on this list stays true. Someone adds an NSG rule, someone gets Contributor for a sprint and keeps it, someone turns on static website hosting to test something. Each one is small and reasonable, and the list quietly stops matching reality.

The shorter way

That repeat is what we built Liberra for. Connect an Azure subscription and it runs a real Prowler scan, the open source one, across the whole subscription. Not our interpretation of Prowler, the actual tool, with its actual checks.

On top of that it keeps an index of what is in the subscription and checks it every sync: NSG rules open to the internet, storage accounts with public access, Cosmos accounts reachable from anywhere. When something changes it is already looked at before you ask.

Then you can just ask. "What is open to the internet right now." "Who has Contributor." In plain English, against your real subscription, with the answer coming from what is there rather than from what the last audit said.

And it cannot delete anything. The delete commands are blocked in the code itself, so the tool you point at your subscription to find problems physically cannot become one. Every change that writes waits for you to approve it. Reads happen immediately, because reading cannot hurt you.

Same checks, same list, running on its own.

Founder, Liberra AI