ACloud.Solutions

Azure security

Azure NSG rules audit: the any-any that was definitely temporary

The rule is at priority 100. It is called temp-allow-rdp. Source is *, destination port is 3389, action is Allow, and it was created on a Friday afternoon in 2022 by somebody who needed to get onto a box quickly and fully intended to remove it.

An azure nsg rules audit is mostly an exercise in finding that rule, and its several cousins, and then working out which of them is load-bearing.

What an azure nsg rules audit should look for

Four things, in descending order of how much they matter.

Inbound Allow from any source on a management port. RDP on 3389, SSH on 22, WinRM on 5985 and 5986, SQL on 1433, and the database ports for whatever else you run. Source of *, 0.0.0.0/0, Internet or Any are all the same finding written four ways, which is one reason this is easy to miss when reading the portal.

Rules with no description. Not a vulnerability. A finding about maintainability, and the single best predictor of whether anyone will dare to remove a rule later. A rule with no description and a name like Rule_1 is one nobody will ever safely delete, so it will outlive the thing it was for.

Overly broad port ranges. 0-65535 on an inbound Allow, or a range that was written to cover two services and now covers three hundred. Also worth checking for ranges specified as a list, because a rule permitting 22,3389,1433 reads as one rule and behaves as three.

Priority collisions and shadowing. Two rules that overlap, where the lower priority number wins and the other one is decoration. Harmless until somebody edits the wrong one and nothing changes, at which point an afternoon disappears.

The rule list is not what flows

This is the part that catches experienced people, and it is worth being precise about.

An NSG rule list tells you what is permitted at that NSG. It does not tell you what actually reaches a machine, because several things sit between the two.

NSGs apply at both subnet and network interface level, and traffic has to be permitted by both. So a permissive subnet rule may be irrelevant because the NIC NSG blocks it, and an audit that reads only one level will report a finding that is not real.

Application Security Groups make the source or destination a membership rather than an address range, so reading the rule alone tells you nothing about scope until you resolve the group.

Service tags such as Internet, AzureCloud and Storage expand to address ranges Microsoft maintains and changes. Internet is not a subtle tag, but AzureCloud is broader than most people assume and includes other tenants' resources.

And a machine with no public IP address and no inbound NAT is not reachable from the internet regardless of what its NSG permits. An NSG allowing 3389 from * on a machine with only a private address is untidy rather than urgent.

So the audit produces candidates. Confirming them means checking whether the machine is reachable at all, which is why the useful output pairs each rule with whether the resources behind it have a public address.

Microsoft's network security group documentation covers the evaluation order and the default rules, and the default rules are worth knowing because AllowVnetInBound explains a lot of traffic people expect to be blocked.

Reading the rules

The shape that works, with the parsing that matters:

Get-AzNetworkSecurityGroup | ForEach-Object {
    $nsg = $_
    $nsg.SecurityRules | Where-Object {
        $_.Direction -eq 'Inbound' -and $_.Access -eq 'Allow'
    } | ForEach-Object {
        # Prefixes and ports are collections, not strings. A rule with several
        # of either looks like one rule and behaves like several.
        $prefixes = @($_.SourceAddressPrefix) + @($_.SourceAddressPrefixes)
        $ports    = @($_.DestinationPortRange) + @($_.DestinationPortRanges)
        [pscustomobject]@{
            NSG         = $nsg.Name
            Rule        = $_.Name
            Priority    = $_.Priority
            Sources     = ($prefixes | Where-Object { $_ }) -join ','
            Ports       = ($ports | Where-Object { $_ }) -join ','
            Description = $_.Description
        }
    }
} | Sort-Object NSG, Priority

The detail worth stealing there is treating the prefix and port fields as collections. SourceAddressPrefix holds a single value and SourceAddressPrefixes holds a list, and a rule uses one or the other. Code that only reads the singular field silently misses every multi-value rule, which are usually the interesting ones. I have written that bug and it reported a clean estate.

What to do with the findings

Not "delete the any-any", at least not first.

Narrow it. Replace * with the office address range, or the VPN range, or a specific address. A rule scoped to something is defensible in a way a rule scoped to everything is not, and narrowing rarely breaks anything because the people using it are coming from somewhere identifiable.

Where a management port genuinely needs to be open to arbitrary sources, that is an argument for Bastion or just-in-time access rather than a better NSG rule. Both remove the standing rule entirely, which is the only version of this that stays fixed.

And write a description on every rule you touch, including the ones you decide to keep. The description is what lets the next person, quite possibly you, tell the difference between a rule that matters and a rule from a Friday afternoon.

The rules that are fine and look alarming

An audit that reports everything permissive as a finding trains people to ignore it, so it is worth knowing which patterns are usually correct.

AllowVnetInBound at priority 65000 permits everything within the virtual network. It is a default rule, it is present on every NSG, and removing it breaks more than it fixes. Report it once as context, not as a finding.

AzureLoadBalancer as a source on a health probe port is how load balancer probes reach a backend. Blocking it takes the pool offline, which is a memorable way to learn this.

Broad rules on a subnet that contains only a Bastion host are the intended design, because Bastion needs specific inbound ranges from Microsoft to work at all.

And a permissive rule on a machine with no public address is untidy rather than exposed, which is the distinction the reachability check exists to make. Sorting findings by whether the resource has a public IP address puts the ones that matter at the top and keeps the rest as a tidying backlog rather than an incident.

Where this sits

NSG rules are one of the ten checks in the AzClean Toolkit, and it reads rather than writes for the obvious reason: a script that edits firewall rules across an estate is a different risk category from one that lists them.

Pair it with the public storage note, because the two findings tend to come from the same original decision. Somebody opened something up temporarily to get a job done, and the job finished. The rest of the azure security notes cover the access and credential side of the same habit.