Back to blog
AzureBest PracticesCloud SecurityIdentity & AccessNetworking

NSG Allows Public VNC: Closing an Open Door on Azure

Learn why an Azure NSG rule allowing public VNC (ports 5900/5500) is a serious risk, and how to lock it down with CLI, Terraform, and Azure Policy.

TL;DR

This check flags Azure NSG rules that allow VNC remote desktop traffic (ports 5900 and 5500) from the public internet. Exposed VNC is a magnet for brute-force attacks and unencrypted session hijacking. Lock the source down to a bastion or VPN range instead of 0.0.0.0/0.

Virtual Network Computing (VNC) is one of those protocols that is incredibly handy when you are setting up a VM and forget all about once it works. The problem is that a forgotten "allow VNC from anywhere" rule on a Network Security Group leaves a graphical remote session sitting wide open on the internet. Lensix raises nsg_openvnc when it finds exactly that.

This post breaks down what the check looks for, why an exposed VNC port is a serious risk, and how to close it cleanly without breaking legitimate access.


What this check detects

The nsg_openvnc check inspects every inbound security rule in your Azure Network Security Groups and flags any rule that meets all of the following conditions:

  • Direction is Inbound and access is Allow
  • Destination port range includes 5900 (VNC server) or 5500 (VNC listening / reverse connections)
  • Source address is a public range such as 0.0.0.0/0, Internet, or *

Note: VNC servers typically listen on 5900 for display :0, with each additional display incrementing the port (5901, 5902, and so on). Port 5500 is used for reverse VNC connections where the viewer listens and the server connects out. The check focuses on 5900 and 5500, but you should treat the whole 5900-5910 band with the same caution.

An NSG rule that triggers this check looks something like this in the Azure CLI output:

{
  "name": "allow-vnc",
  "direction": "Inbound",
  "access": "Allow",
  "protocol": "Tcp",
  "sourceAddressPrefix": "0.0.0.0/0",
  "destinationPortRange": "5900",
  "priority": 320
}

Why it matters

VNC was designed for convenience on trusted networks, not for hardened exposure to the open internet. Several characteristics make a public VNC port genuinely dangerous.

Weak and legacy authentication

The classic VNC authentication scheme uses a challenge-response based on DES with an 8-character password limit. That is trivially brute-forceable by modern standards, and many VNC servers ship with authentication that can be downgraded or bypassed. Some popular VNC implementations have had outright authentication-bypass vulnerabilities over the years.

Sessions are often unencrypted

Base VNC traffic is not encrypted. Without an additional TLS tunnel or SSH wrapper, anyone positioned on the network path can capture keystrokes, watch the screen, and read whatever the operator is doing. A public-facing VNC session is effectively broadcasting an interactive desktop.

It is actively scanned

Internet-wide scanners hit ports 5900 and 5500 constantly. Search engines like Shodan index exposed VNC endpoints, and some even capture screenshots of unauthenticated sessions. Once your IP shows up, automated brute-force tooling follows within minutes.

Danger: A successful VNC compromise gives an attacker a full interactive graphical session on the host, exactly as if they were sitting at the keyboard. From there they can pivot into your VNet, harvest credentials, and move toward your data plane. This is one of the most direct paths to full host takeover.

Real-world impact

Exposed VNC has been the entry point in numerous ransomware and cryptomining incidents. Unlike an exposed database that needs a query to do damage, a VNC session lets the attacker simply act as a user, install tooling, disable defenses, and stage data exfiltration through a familiar desktop.


How to fix it

The goal is to remove public access while keeping legitimate administrative access working. You have a few options depending on how you manage NSGs.

Step 1: Find the offending rule

az network nsg rule list \
  --resource-group my-rg \
  --nsg-name my-nsg \
  --query "[?destinationPortRange=='5900' || destinationPortRange=='5500'].{name:name, src:sourceAddressPrefix, port:destinationPortRange, access:access}" \
  --output table

Step 2: Restrict the source to a trusted range

If you genuinely need VNC, scope the rule to a bastion host, VPN egress IP, or corporate CIDR rather than the entire internet.

az network nsg rule update \
  --resource-group my-rg \
  --nsg-name my-nsg \
  --name allow-vnc \
  --source-address-prefixes 203.0.113.10/32 \
  --destination-port-ranges 5900

Step 3: Or remove the rule entirely

Danger: Deleting a rule takes effect immediately. If a real operator is relying on this path, confirm an alternative such as Azure Bastion or a VPN is in place before you run this, or you will lock yourself out of the VM.

az network nsg rule delete \
  --resource-group my-rg \
  --nsg-name my-nsg \
  --name allow-vnc

Step 4: Prefer a bastion over direct exposure

The better long-term fix is to stop exposing remote-access ports at all. Azure Bastion provides browser-based RDP and SSH over TLS without opening any inbound ports on the VM's NSG. For VNC specifically, tunnel it over SSH through the bastion rather than publishing 5900.

# Tunnel VNC over an SSH connection instead of exposing 5900
ssh -L 5900:localhost:5900 user@bastion-host
# Then point your VNC viewer at localhost:5900

Tip: If you find yourself needing temporary VNC access, use a just-in-time approach. Azure Defender for Cloud's just-in-time VM access opens the port only for a defined window and source IP, then closes it automatically. No more stale "allow from anywhere" rules drifting in your subscription.


Fixing it with infrastructure as code

If your NSGs are managed in Terraform or Bicep, fix the source there so the change does not get reverted on the next apply.

Terraform

resource "azurerm_network_security_rule" "vnc" {
  name                        = "allow-vnc"
  priority                    = 320
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "5900"
  source_address_prefix       = "203.0.113.10/32"  # trusted range, not 0.0.0.0/0
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.main.name
  network_security_group_name = azurerm_network_security_group.main.name
}

Bicep

{
  "name": "allow-vnc",
  "properties": {
    "priority": 320,
    "direction": "Inbound",
    "access": "Allow",
    "protocol": "Tcp",
    "sourcePortRange": "*",
    "destinationPortRange": "5900",
    "sourceAddressPrefix": "203.0.113.10/32",
    "destinationAddressPrefix": "*"
  }
}

How to prevent it from happening again

One-off remediation is not enough. Without a guardrail, the rule comes back the next time someone is debugging a VM at 2 a.m. Put controls in place at three levels.

Azure Policy to deny public VNC

Azure Policy can audit or outright deny NSG rules that expose VNC to the internet. Use a custom policy that matches inbound allow rules with public sources on ports 5900 or 5500.

{
  "if": {
    "allOf": [
      { "field": "type", "equals": "Microsoft.Network/networkSecurityGroups/securityRules" },
      { "field": "Microsoft.Network/networkSecurityGroups/securityRules/access", "equals": "Allow" },
      { "field": "Microsoft.Network/networkSecurityGroups/securityRules/direction", "equals": "Inbound" },
      { "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", "in": ["5900", "5500"] },
      { "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", "in": ["*", "0.0.0.0/0", "Internet"] }
    ]
  },
  "then": { "effect": "deny" }
}

Warning: Roll out a deny policy in audit mode first. If you flip straight to deny across a busy subscription, you can block legitimate deployments and trigger confusing pipeline failures. Audit, review the findings, fix the real exposures, then enforce.

Catch it in CI/CD

Scan IaC before it ever reaches Azure. Tools like Checkov, tfsec, and Terrascan flag overly permissive NSG rules in pull requests.

# Fail the build if a public ingress rule slips into the plan
checkov -d ./infra --framework terraform

Continuous monitoring with Lensix

Policy and CI gates cover what you provision through code, but manual portal changes and emergency hotfixes still happen. Lensix runs nsg_openvnc continuously across your subscriptions so a rule added by hand surfaces in your findings instead of sitting unnoticed until a scanner finds it first.


Best practices

  • Never expose remote-access ports to 0.0.0.0/0. This applies to VNC (5900/5500), RDP (3389), and SSH (22) alike.
  • Use a bastion or VPN. Azure Bastion removes the need for any public inbound rule on the VM.
  • Encrypt VNC traffic. If VNC must be used, tunnel it over SSH or TLS. Never run base VNC across an untrusted path.
  • Apply just-in-time access. Open management ports only for the window and source you need.
  • Audit NSG rules regularly. Stale "temporary" rules are the single most common source of this finding.
  • Use Application Security Groups. Group VMs by role and write rules against the group rather than scattering individual IP rules that are hard to audit.

If a port does not need to be reachable from the internet, it should not be. Public VNC is almost never the right answer, and when it is, it should be wrapped in encryption and locked to a known source.

Close the rule, move administrative access behind a bastion, and add a policy gate so it cannot creep back in. That turns a recurring finding into a solved problem.