To find network security group (NSG) rules open to the internet in Azure, query Azure Resource Graph for inbound Allow rules whose source is any address, the Internet service tag or 0.0.0.0/0, then look for rules that open SSH, RDP or other management ports. The list shows what rules allow, not what can be reached: a rule with a lower priority number that denies the same traffic wins.
| Method | Best fit |
|---|---|
| PowerShell | A repeatable CSV across enabled subscriptions, with management ports flagged. |
| Admin centre | A quick review in Azure Resource Graph Explorer, or 1 group's rules, without installing tools. |
| Altivane | Recommended for recurring reviews across readable subscriptions, with management ports marked in a workbook to keep. |
Before you start
Use an Azure account with read access to the subscriptions you want to check. The built-in Reader role, assigned at the subscription or a parent scope, is enough for Resource Graph queries. The PowerShell example uses Az.Accounts and Az.ResourceGraph. Microsoft's Resource Graph prerequisites do not list a separate product licence.
The query expands each group's custom rules (securityRules) and keeps those that are Inbound, Allow and have a source of *, Internet, 0.0.0.0/0 or any, alone or in a list. Other sources, such as a very wide address range, are not counted. The script flags a rule when its ports include 22 (SSH), 3389 (RDP), 5985 or 5986 (WinRM), as a port, a range or *.
The examples only read Azure and export a CSV. Resource Graph is near real-time and eventually consistent, so a recent change may not show yet. The script paginates beyond 1,000 results, and its finally block disconnects the account and removes its local contexts, so you may need to sign in again.
Option 1
Find NSG rules open to the internet with PowerShell
Install the modules once, then run the script, which adds a management port flag to each rule.
Install-Module -Name Az.Accounts, Az.ResourceGraph -Repository PSGallery -Scope CurrentUser$ErrorActionPreference = 'Stop'
Connect-AzAccount -ErrorAction Stop
try {
$subscriptionIds = @(
Get-AzSubscription |
Where-Object State -eq 'Enabled' |
Select-Object -ExpandProperty Id
)
if ($subscriptionIds.Count -eq 0) {
Write-Output 'No enabled subscriptions are available to this account.'
return
}
$query = @'
Resources
| where type =~ 'microsoft.network/networksecuritygroups'
| mv-expand rule = properties.securityRules limit 2000
| where rule.properties.direction =~ 'Inbound' and rule.properties.access =~ 'Allow'
| extend srcOne = tostring(rule.properties.sourceAddressPrefix),
srcMany = tostring(rule.properties.sourceAddressPrefixes)
| where srcOne in~ ('*', 'Internet', '0.0.0.0/0', 'any')
or srcMany contains '"*"' or srcMany contains '"Internet"'
or srcMany contains '"0.0.0.0/0"' or srcMany contains '"any"'
| project id, name, subscriptionId, resourceGroup, location,
ruleName = tostring(rule.name),
priority = toint(rule.properties.priority),
protocol = tostring(rule.properties.protocol),
port = tostring(rule.properties.destinationPortRange),
portList = rule.properties.destinationPortRanges,
source = srcOne, sourceList = rule.properties.sourceAddressPrefixes
| order by subscriptionId asc, name asc, priority asc
'@
function Get-AzGraphRows {
param([string]$Query, [string[]]$SubscriptionIds)
$results = [System.Collections.Generic.List[object]]::new()
$skipToken = $null
do {
$arguments = @{
Query = $Query
Subscription = $SubscriptionIds
First = 1000
}
if ($skipToken) { $arguments.SkipToken = $skipToken }
$page = Search-AzGraph @arguments
foreach ($record in $page.Data) { $results.Add($record) }
$skipToken = $page.SkipToken
} while ($skipToken)
return $results.ToArray()
}
function Test-ManagementPort {
param([string[]]$Ports)
foreach ($port in $Ports) {
if ($port -eq '*') { return $true }
if ($port -match '^(\d+)(?:-(\d+))?$') {
$low = [int]$Matches[1]
$high = if ($Matches[2]) { [int]$Matches[2] } else { $low }
foreach ($managed in 22, 3389, 5985, 5986) {
if ($managed -ge $low -and $managed -le $high) { return $true }
}
}
}
return $false
}
$rows = @(Get-AzGraphRows -Query $query -SubscriptionIds $subscriptionIds)
if ($rows.Count -eq 0) {
Write-Output 'No inbound rules open to the internet were found in the indexed groups.'
} else {
$report = foreach ($row in $rows) {
$ports = @(@($row.port) + @($row.portList) | Where-Object { $_ })
$sources = @(@($row.source) + @($row.sourceList) | Where-Object { $_ })
[pscustomobject]@{
Subscription = $row.subscriptionId
ResourceGroup = $row.resourceGroup
NetworkSecurityGroup = $row.name
Location = $row.location
Rule = $row.ruleName
Priority = $row.priority
Protocol = $row.protocol
Ports = $ports -join ', '
Source = $sources -join ', '
ManagementPort = if (Test-ManagementPort $ports) { 'Yes' } else { 'No' }
ResourceId = $row.id
}
}
$report | Sort-Object @{ Expression = 'ManagementPort'; Descending = $true },
Subscription, NetworkSecurityGroup, Priority |
Export-Csv -Path './nsg-rules-open-to-internet.csv' -NoTypeInformation -Encoding utf8
$management = @($report | Where-Object ManagementPort -eq 'Yes').Count
Write-Output "$($report.Count) rules saved to nsg-rules-open-to-internet.csv."
Write-Output "$management of them open a management port."
}
} finally {
Disconnect-AzAccount | Out-Null
}The CSV has 1 row for each rule, with its group, subscription, location, priority, protocol, ports, sources, management port flag and resource ID. Rules with several ports or sources stay 1 row, and management port rows come first. An empty result produces a message, not an empty CSV.
To narrow the search, keep only the subscription IDs you want in $subscriptionIds. To widen it, remove the source test from the query to list every inbound Allow rule. The query keeps id because Azure's continuation token requires it for pagination.
Option 2
Review open NSG rules in Azure Resource Graph Explorer
- Sign in to the Azure portal with read access to the subscriptions.
- Search for resource graph > choose Resource Graph Explorer.
- To change its scope, choose Directory, then choose the directory, management group or subscription to query.
- Paste this query in Query 1 and choose Run query:
Resources | where type =~ 'microsoft.network/networksecuritygroups' | mv-expand rule = properties.securityRules limit 2000 | where rule.properties.direction =~ 'Inbound' and rule.properties.access =~ 'Allow' | extend srcOne = tostring(rule.properties.sourceAddressPrefix), srcMany = tostring(rule.properties.sourceAddressPrefixes) | where srcOne in~ ('*', 'Internet', '0.0.0.0/0', 'any') or srcMany contains '"*"' or srcMany contains '"Internet"' or srcMany contains '"0.0.0.0/0"' or srcMany contains '"any"' | project id, name, subscriptionId, resourceGroup, location, ruleName = tostring(rule.name), priority = toint(rule.properties.priority), protocol = tostring(rule.properties.protocol), port = tostring(rule.properties.destinationPortRange), portList = rule.properties.destinationPortRanges, source = srcOne, sourceList = rule.properties.sourceAddressPrefixes | order by subscriptionId asc, name asc, priority asc - Review the rules in Results, looking in
portandportListfor 22, 3389, 5985, 5986, a range that includes them, or*. Download as CSV keeps the rows, up to 55,000 records.
Check 1 group
Search for Network security group > choose Network security groups > select the group > choose Inbound security rules.
Explorer suits an occasional look; use PowerShell for the management port flag or a repeatable CSV. The group page suits 1 group you already suspect.
Option 3 ยท Recommended for recurring reviews
Review open NSG rules with Altivane
- Open Altivane and choose the tenant.
- Run the Azure Network Rules Open to the Internet report.
- Open the Excel workbook and read Inbound rules open to the internet: each rule's priority, protocol, ports and source, whether it opens a management port, and what the group is attached to.
- Read Public IP addresses to see each address and what holds it.
Altivane reads every subscription it can in 1 run and writes an Excel workbook to keep. The Management port column marks SSH, RDP and WinRM rules, including ranges and any port, and a group attached to nothing says so. A subscription it cannot read is left out and the report is marked partial.
Contact usCommon questions
Does a rule in the result mean the virtual machine is reachable from the internet?
Not always. Azure processes rules in priority order, lowest number first, and stops at the first match, so a deny rule with a lower number wins. Security admin rules from Azure Virtual Network Manager are evaluated before NSG rules. Treat each row as a rule to review, then check the effective rules for the machine.
What should replace an open RDP or SSH rule?
Microsoft advises never allowing RDP (3389) or SSH (22) inbound from 0.0.0.0/0, and offers Azure Bastion, a point-to-site VPN or just-in-time VM access instead. This guide only reads rules, so confirm who uses a rule before changing it.
Which public IP addresses are not attached to anything?
That is a separate check. See how to find unused public IP addresses in Azure.
Microsoft references
Technical references checked on 7 October 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.
- Install-Module cmdlet reference
- Connect-AzAccount cmdlet reference
- Get-AzSubscription cmdlet reference
- Disconnect-AzAccount cmdlet reference
- Search-AzGraph cmdlet reference
- Run a Resource Graph query in the Azure portal
- Azure Resource Graph query language and tables
- Azure Resource Graph overview
- Azure network security groups overview
- Create, change, or delete a network security group
- Enable just-in-time access
- Developer and admin access to Azure VMs
- Azure built-in roles: Reader