Back to blog
AzureBest PracticesCloud SecurityCompute & ContainersNetworking

NSG Allows Public Docker: Closing the Open Docker Daemon Port on Azure

An Azure NSG rule exposing Docker ports 2375/2376 to the internet is remote root on your VM. Learn how to detect, fix, and prevent this critical misconfiguration.

TL;DR

This check flags Azure NSG rules that expose the Docker daemon API (ports 2375/2376) to the public internet. An open Docker socket is effectively remote root on the host, so restrict the rule to trusted source ranges or remove the inbound rule entirely.

The Docker daemon listening on a TCP port is one of those conveniences that turns into a full host compromise the moment it touches the public internet. If an Azure Network Security Group (NSG) rule allows inbound traffic to ports 2375 or 2376 from 0.0.0.0/0 or Internet, anyone who can reach that VM can talk to the Docker API. And the Docker API, by design, lets you do almost anything on the underlying host.

This Lensix check, nsg_opendocker in the nsg_checks module, scans your Azure NSGs for inbound rules that permit Docker daemon ports from untrusted sources.


What this check detects

Lensix inspects every NSG in your subscription and looks for inbound security rules that meet all of the following conditions:

  • Access is set to Allow
  • Direction is Inbound
  • Destination port range includes 2375 (unencrypted Docker API) or 2376 (TLS Docker API)
  • Source is a public range such as 0.0.0.0/0, *, Internet, or any broad CIDR that resolves to the open internet

A rule that matches all four is the textbook misconfiguration: a remotely accessible Docker control plane with no network boundary in front of it.

Note: Port 2375 is the legacy, unencrypted Docker TCP socket. Port 2376 is the TLS-enabled variant. Exposing 2376 is less catastrophic than 2375 because it expects client certificates, but it is still a finding. A misconfigured or disabled TLS verification turns 2376 into the same problem as 2375.


Why it matters

The Docker daemon API is not a normal web service. When you can reach it, you can create containers, mount host paths, and run privileged workloads. That is the equivalent of root on the VM. There is no authentication on a plain 2375 socket, so exposing it publicly hands attackers a direct path to the host.

Here is the standard attack flow once an exposed daemon is found, usually by an internet-wide scanner like Shodan or a quick masscan sweep:

  1. The attacker connects to tcp://your-vm:2375 and confirms the API responds.
  2. They launch a container that bind-mounts the host filesystem, for example mounting / into /host inside the container.
  3. From inside that container they read secrets, write a cron job or SSH key, or chroot into the host filesystem.
  4. Game over. They now have root-level persistence on the VM, and from there can pivot into the rest of your VNet.

A real example of what an attacker runs after finding the open socket:

docker -H tcp://VICTIM_IP:2375 run -v /:/host -it alpine chroot /host sh

That single command gives them a root shell on the host. The most common follow-up is cryptomining, since the payload is trivial to deploy at scale, but data theft and lateral movement are just as easy.

Danger: An open 2375 port is not a low-priority hardening item. It is remote code execution as root with zero authentication. Treat any public Docker daemon finding as an active incident until you confirm the host has not been compromised.


How to fix it

The fix has two parts: lock down the network rule, and reconsider why the daemon is listening on TCP at all.

Step 1: Find the offending rule

List the rules in the NSG and look for anything touching 2375 or 2376:

az network nsg rule list \
  --resource-group myResourceGroup \
  --nsg-name myNsg \
  --query "[?destinationPortRange=='2375' || destinationPortRange=='2376']" \
  --output table

Step 2: Remove or scope down the rule

If nothing legitimately needs remote Docker access, delete the rule entirely:

Warning: Deleting or tightening this rule will immediately cut off any remote tooling that connects to the Docker daemon over TCP, including some CI runners and orchestration scripts. Confirm what depends on it before you change it, ideally during a maintenance window.

az network nsg rule delete \
  --resource-group myResourceGroup \
  --nsg-name myNsg \
  --name Allow-Docker-2375

If you genuinely need remote access (for example a build server in a known subnet), restrict the source to a specific trusted CIDR instead of the internet:

az network nsg rule update \
  --resource-group myResourceGroup \
  --nsg-name myNsg \
  --name Allow-Docker-2376 \
  --source-address-prefixes 10.0.1.0/24 \
  --destination-port-ranges 2376 \
  --protocol Tcp \
  --access Allow

Step 3: Stop exposing the daemon over TCP

Fixing the NSG closes the door, but the daemon is still listening. The cleaner fix is to not bind Docker to a TCP socket at all. Check the daemon configuration on the host:

cat /etc/docker/daemon.json

If you see a tcp://0.0.0.0:2375 entry in hosts, remove it so Docker only listens on the local Unix socket:

{
  "hosts": ["unix:///var/run/docker.sock"]
}

Then reload and restart the daemon:

sudo systemctl daemon-reload
sudo systemctl restart docker

Tip: If you need remote Docker access from a laptop or CI runner, use docker context over SSH instead of opening a TCP port. Run docker context create remote --docker "host=ssh://user@host" and you get authenticated, encrypted access for free with no daemon port exposed.

Fix it with Infrastructure as Code

If your NSGs are managed in Terraform, scope the source explicitly rather than using a wildcard. Here is a corrected rule for trusted internal access only:

resource "azurerm_network_security_rule" "docker_tls" {
  name                        = "Allow-Docker-2376-Internal"
  priority                    = 200
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "2376"
  source_address_prefix       = "10.0.1.0/24"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.main.name
  network_security_group_name = azurerm_network_security_group.main.name
}

How to prevent it from happening again

Manual cleanup only works until the next person copies a Stack Overflow answer into a daemon config. Push the guardrail left into your pipeline and your platform.

Azure Policy to deny the rule

Use Azure Policy to block NSG rules that allow Docker ports from the internet. A custom policy can audit or deny any inbound rule with a destination port in the 2375-2376 range and a source of * or Internet:

{
  "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": ["2375", "2376"] },
      { "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", "in": ["*", "0.0.0.0/0", "Internet"] }
    ]
  },
  "then": { "effect": "deny" }
}

Catch it in CI/CD

Scan Terraform plans before they apply. Tools like Checkov and tfsec flag open management ports out of the box. A quick pre-merge gate looks like this:

checkov -d . --framework terraform --compact

For custom enforcement, an OPA Conftest policy can reject any plan that opens 2375 or 2376 to a wildcard source, failing the pipeline before the rule ever reaches Azure.

Tip: Pair the pipeline scan with continuous monitoring in Lensix. Pipeline gates catch new IaC, but they miss rules created by hand in the portal or by a script outside your repo. Lensix re-scans live NSGs, so a click-ops change that opens 2375 still gets caught.


Best practices

  • Never bind the Docker daemon to a public TCP socket. Default to the Unix socket and use SSH-based contexts for remote access.
  • Avoid management ports on the open internet entirely. This applies to 2375/2376, but also to 22, 3389, 5985, and friends. Front them with a bastion, Azure Bastion, or a VPN.
  • Use TLS with client certificate verification if you must expose 2376, and confirm tlsverify is actually enabled rather than just present in the config.
  • Apply least privilege to NSG sources. Replace every * source with the narrowest CIDR that works.
  • Prefer a managed orchestrator. If you are running containers on raw VMs to get remote control, AKS or Azure Container Apps gives you a managed control plane without exposing any daemon socket.
  • Assume compromise on any historical exposure. If 2375 was ever open, audit the host for unexpected containers, cron jobs, and outbound connections before closing the case.

The pattern here is simple: the Docker daemon is a root-equivalent control plane, so it should never be one NSG rule away from the public internet. Close the port, prefer SSH contexts for remote access, and add a policy gate so it stays closed.