Skip to content

Azure Sentinel honeypot

An exposed Windows VM took 3,516 failed RDP logons from 8 countries in 24 hours, 3,106 of them from a single Indonesian IP.

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 3,516 failed logons. One IP address in Indonesia was responsible for 3,106 of them.

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 first 24 hours:

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

  • Automated brute force starts within minutes of a public IP appearing, not days.
  • The volume is dominated by a few sources. One address did 88% of the work, 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.