Skip to content

Azure Sentinel honeypot

An exposed Windows VM in Azure took more than 3,500 failed RDP logons in 24 hours, 3,106 of them from a single IP address in Indonesia.

Date
Stack
Azure, Microsoft Sentinel, Log Analytics, Azure Monitor Agent, KQL

The finding

Within 24 hours of putting a Windows Server VM on the public internet with RDP open, Sentinel had recorded more than 3,500 failed logons. One IP address in Indonesia was responsible for 3,106 of them. No logon succeeded, and I shut the VM down after the 24-hour run.

Context

I wanted to see what unsolicited traffic looks like against a machine nobody has been told about, and to practice the full path from a raw Windows security event to a Sentinel workbook without any middleware in between.

What was built

  • A Windows Server 2025 VM in Azure with every inbound network security group rule open and the Windows firewall disabled, so RDP on port 3389 was reachable from anywhere.
  • The Azure Monitor Agent, installed through a data collection rule, streaming Windows security events into a Log Analytics workspace.
  • Microsoft Sentinel on that workspace.
  • A workbook that enriches each attacker IP with country, city, latitude and longitude using the built-in geo_info_from_ip_address() function, then plots attempts on a map and ranks them by country.

Evidence

World map of failed logon sources, with one large marker over Indonesia and small markers in Egypt, Romania, Saudi Arabia, South Korea, the United States, France and Russia.

Failed logons (event 4625) by country over the 24 hours, as the workbook’s bar chart rendered them:

Country Attempts
Indonesia 3,106
Egypt 303
Romania 55
Saudi Arabia 31
United States 13
South Korea 6
France 1
Russia 1

The map query:

SecurityEvent
| where EventID == 4625
| where isnotempty(IpAddress)
| extend geoInfo = geo_info_from_ip_address(IpAddress)
| extend Country = tostring(geoInfo.country)
| extend Latitude = todouble(geoInfo.latitude)
| extend Longitude = todouble(geoInfo.longitude)
| extend City = tostring(geoInfo.city)
| summarize AttackCount = count() by Country, City, Latitude, Longitude, IpAddress

Top source addresses:

SecurityEvent
| where EventID == 4625
| where isnotempty(IpAddress)
| extend Country = tostring(geo_info_from_ip_address(IpAddress).country)
| where isnotempty(Country)
| summarize Attacks = count() by IpAddress, Country
| sort by Attacks desc

Query results table listing attacker IP addresses, countries and attempt counts, led by 114.10.44.29 in Indonesia with 3,106.

I also kept a query running for the thing I did not want to see: a successful logon (event 4624) over the network or RDP from a public address.

SecurityEvent
| where EventID == 4624
| where LogonType in (3, 10)
| where IpAddress != "-"
| where IpAddress !startswith "10."
| where IpAddress !startswith "172."
| where IpAddress !startswith "192.168."
| where IpAddress != "127.0.0.1"
| where Account !contains "SYSTEM"
| where Account !contains "LOCAL SERVICE"
| where Account !contains "NETWORK SERVICE"
| where Account !contains "$"
| where Account != "NT AUTHORITY\\ANONYMOUS LOGON"
| project TimeGenerated, Account, IpAddress, LogonType, Computer
| order by TimeGenerated desc

It returned nothing during the run.

What it means

  • The volume is dominated by a few sources. One address did close to nine in ten of the attempts, so per-IP lockout or rate limiting would have removed most of the noise, while country blocking would have missed the long tail.
  • geo_info_from_ip_address() is enough for enrichment at this scale. No external API, no function app, no cost beyond ingestion.
  • A workbook is a short path from raw events to something a non-analyst can read.