Asegurar RDP: proteger un Windows Server frente a ataques
Un Windows Server recién instalado acumula en pocas horas miles de inicios de sesión RDP fallidos. Esta guía muestra qué medidas funcionan de verdad, qué cuesta cada una y cómo evitar dejarte fuera a ti mismo.
Un servidor Windows que sale a la red con el puerto 3389 abierto no acaba siendo atacado en algún momento: lo es en cuestión de minutos. Los escáneres funcionan de forma permanente, conocen todos los rangos de direcciones IPv4 y van probando nombres de usuario y contraseñas sin descanso. Casi todos los incidentes de ransomware en servidores pequeños empiezan exactamente aquí: una cuenta llamada Administrator, una contraseña que alguien consideró suficiente y ningún bloqueo después del intento fallido número mil.
Esta guía repasa las medidas en el orden de su eficacia real, no en el orden en el que aparecen en la mayoría de los artículos. Todos los comandos se ejecutan en una sesión de PowerShell con permisos de administrador, desde Windows Server 2016 hasta 2025.
Por qué RDP es la puerta más atacada
RDP no es un mal protocolo. El problema es que expone un formulario de inicio de sesión completo a Internet abierto y que Windows, de forma predeterminada, deja rellenarlo un número ilimitado de veces. Un atacante no necesita ninguna vulnerabilidad, solo paciencia y una lista de contraseñas.
En una sola línea puedes ver hasta qué punto está afectado tu servidor. Cuenta los inicios de sesión fallidos de las últimas 24 horas:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-1)} -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count
Un servidor interno sin acceso desde Internet se mueve normalmente en cifras bajas de dos dígitos al día, casi siempre contraseñas olvidadas y cuentas de servicio antiguas. Un servidor con el 3389 abierto llega enseguida a cifras de cuatro o cinco dígitos. Esa cifra es tu referencia: después de las medidas de más abajo debería caer en varios órdenes de magnitud. Si Get-WinEvent responde "No se encontraron eventos que coincidan con los criterios de selección especificados", tu servidor no está registrando los inicios de sesión fallidos. Eso lo arreglamos más abajo.
Antes del primer cambio: asegura la vía de vuelta
Cada una de las medidas siguientes puede dejarte fuera. En un servidor al que solo llegas por RDP, eso significa el final de la sesión y el principio de una noche larga. Tres precauciones cuestan cinco minutos y evitan justo eso.
Primero: comprueba que dispones de una consola que funcione al margen de RDP. En los servidores root KVM de KernelHost encuentras en el área de cliente una consola VNC que mira directamente a la pantalla de la máquina virtual. Sigue funcionando incluso cuando el firewall, la red y el servicio RDP están rotos a la vez. Ábrela una vez a modo de prueba antes de cambiar nada, no después.
Segundo: crea una segunda cuenta de administrador, para que una cuenta bloqueada no equivalga a un servidor perdido. Los nombres de los grupos dependen del idioma, así que trabajamos con los SID fijos (S-1-5-32-544 es el grupo de administradores locales y S-1-5-32-555 el de usuarios de Escritorio remoto):
New-LocalUser -Name 'kh-adm' -Password (Read-Host -AsSecureString -Prompt 'Contraseña') -FullName 'Cuenta de mantenimiento' -PasswordNeverExpires
Add-LocalGroupMember -SID 'S-1-5-32-544' -Member 'kh-adm'
Add-LocalGroupMember -SID 'S-1-5-32-555' -Member 'kh-adm'
La tercera línea es, en rigor, innecesaria, porque los miembros del grupo de administradores locales entran por RDP de todos modos. Pero no hace daño y deja la intención a la vista por si más adelante sacas esa cuenta del grupo de administradores.
Tercero: deja abierta durante todo el cambio la sesión RDP en la que estás trabajando y prueba cada modificación con una segunda conexión nueva. Las reglas de firewall nuevas no cortan de inmediato una sesión existente, mientras que una conexión nueva falla al instante. Así detectas el error mientras todavía puedes deshacerlo.
Forzar la autenticación a nivel de red
Sin autenticación a nivel de red (en inglés Network Level Authentication, NLA), el servidor primero levanta una sesión, dibuja una pantalla de inicio de sesión y pregunta después por las credenciales. Cada intento de conexión anónimo cuesta así memoria RAM y CPU, y todo el código que hay detrás de la pantalla de inicio de sesión queda accesible antes de cualquier autenticación. Justo ahí estaban las vulnerabilidades RDP graves del pasado.
Con NLA, el servidor comprueba las credenciales mediante CredSSP antes de que llegue a existir una sesión. Un bot que no tenga credenciales válidas no obtiene más que una conexión TLS rechazada.
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 1
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'SecurityLayer' -Value 2
SecurityLayer con el valor 2 fuerza TLS en la negociación. El valor 1 (negociar) permite que un cliente vuelva si hace falta a la antigua capa de seguridad de RDP, y 0 es esa capa antigua sin TLS. Para comprobarlo:
Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' | Select-Object UserAuthentication, SecurityLayer
No hace falta reiniciar, los valores se aplican a cada conexión que se establece de nuevo. Las sesiones existentes siguen igual, lo que resulta práctico si acabas de dejar fuera a un cliente.
Cuando después ya no entra nadie
Después de activar NLA aparecen con regularidad dos síntomas típicos.
"El equipo remoto requiere autenticación a nivel de red, que su equipo no admite. Para obtener ayuda, póngase en contacto con el administrador del sistema o con el soporte técnico." El cliente es demasiado antiguo o no habla CredSSP. Los clientes de Windows actuales llevan años pudiendo hacerlo. En Linux, FreeRDP exige la opción /sec:nla, Remmina que el protocolo de seguridad esté puesto en NLA, y los clientes de macOS muy antiguos fallan siempre. La solución pasa siempre por un cliente más reciente, nunca por desactivar NLA.
El inicio de sesión falla con la contraseña correcta. Esta es la trampa que la mayoría de las guías se callan: si en una cuenta está marcada la opción "El usuario debe cambiar la contraseña en el siguiente inicio de sesión" o la contraseña ha caducado, esa cuenta ya no puede iniciar sesión en absoluto con NLA activo. CredSSP no sabe hacer cambios de contraseña, y está pensado así. Según la versión, el servidor informa de un error de autenticación o simplemente de credenciales incorrectas. La salida pasa por la consola del área de cliente, o bien excluyes las cuentas de mantenimiento del plazo de caducidad:
Set-LocalUser -Name 'kh-adm' -PasswordNeverExpires $true
Bloqueo de cuentas: el interruptor individual más eficaz
Una contraseña robusta protege frente a las adivinanzas, un bloqueo de cuentas protege frente a las adivinanzas ilimitadas. Sin él, un atacante puede lanzar millones de intentos por semana; con él, son diez cada cuarto de hora. Fija el umbral, la duración del bloqueo y la ventana de observación en una sola llamada, porque si no Windows rechaza la duración mientras el umbral siga en 0:
net accounts /lockoutthreshold:10 /lockoutduration:15 /lockoutwindow:15
La duración del bloqueo tiene que ser siempre mayor o igual que la ventana de observación. Para controlar el resultado:
net accounts
Las versiones más nuevas de Windows ya traen un valor predeterminado de ese orden de magnitud; las instalaciones antiguas y muchas imágenes de proveedores, no. Comprobarlo no cuesta nada.
Incluir la cuenta de administrador integrada
Históricamente, justo la cuenta que todo atacante prueba primero estaba excluida del bloqueo. Para eso existe la directiva "Permitir el bloqueo de la cuenta de administrador". Eso sí, requiere una versión reciente, en concreto Windows 11 22H2 o Windows Server 2025. En un Windows Server 2022 (build 20348), la exportación de la directiva de seguridad no contiene en absoluto la línea AllowAdministratorLockout: en la sección [System Access] solo aparecen ahí valores como LockoutBadCount y MinimumPasswordLength. Comprueba por eso primero qué exporta realmente tu sistema:
secedit /export /cfg C:\secpol.inf
Select-String -Path C:\secpol.inf -Pattern 'AllowAdministratorLockout'
Si aquí no aparece ninguna coincidencia, tu build no conoce esa directiva y esta sección está resuelta para ti. Justo aquí está la trampa que generan la mayoría de las guías: el típico comando de una línea con -replace sustituye una cadena que ni siquiera existe en el archivo, pero no informa de ningún error, y el secedit /configure posterior termina con éxito. Después crees que el bloqueo de la cuenta de administrador está activo, aunque no ha cambiado nada.
En un build que sí conoce la directiva, modificas la línea si existe y la insertas si no. Esta versión cubre los dos casos:
$c = Get-Content C:\secpol.inf
if ($c -notmatch 'AllowAdministratorLockout') { $c = $c -replace '(LockoutBadCount = \d+)', "`$1`r`nAllowAdministratorLockout = 1" } else { $c = $c -replace 'AllowAdministratorLockout = 0','AllowAdministratorLockout = 1' }
$c | Set-Content C:\secpol.inf -Encoding Unicode
El -Encoding Unicode es obligatorio y no es ningún detalle: el archivo INF tiene que estar en UTF-16 LE con marca de orden de bytes. En Windows PowerShell 5.1, Set-Content escribe si no en ANSI, y secedit rechaza el archivo. Antes de volver a escribirlo, secedit /validate C:\secpol.inf comprueba el archivo, y después:
secedit /configure /db C:\Windows\security\local.sdb /cfg C:\secpol.inf /areas SECURITYPOLICY
Y después relee sin falta el resultado, porque una ejecución correcta de secedit no demuestra nada en este punto. Exporta la directiva otra vez a un segundo archivo y mira si el valor está realmente ahí con un 1.
En un controlador de dominio, estos ajustes locales no se aplican. Allí la directiva de bloqueo va en la Default Domain Policy, en Configuración del equipo, Configuración de Windows, Configuración de seguridad, Directivas de cuenta.
La otra cara y cómo volver a entrar
Un bloqueo de cuentas es también un arma en tu contra: quien conozca tu nombre de usuario puede mantenerlo bloqueado de forma permanente enviando diez contraseñas falsas cada 15 minutos. Justo por eso el bloqueo es solo la segunda línea de defensa; la primera es la restricción por IP de la sección siguiente.
Si una cuenta local está bloqueada, el cliente informa en esencia de que "La cuenta a la que se hace referencia está bloqueada actualmente y no se puede iniciar sesión en ella". La vuelta más sencilla es esperar, porque Windows la desbloquea por sí solo cuando vence la duración del bloqueo. Si no quieres esperar, desbloquea desde la consola:
$u = [ADSI]"WinNT://./kh-adm,user"; $u.IsAccountLocked = $false; $u.SetInfo()
En Active Directory es más corto con Unlock-ADAccount -Identity kh-adm.
Contraseñas que hacen que el bloqueo tenga sentido
Diez intentos por cuarto de hora solo son un obstáculo si la contraseña no ocupa el tercer puesto de todas las listas. La longitud mínima y la complejidad las fijas así:
net accounts /minpwlen:14
La regla de complejidad está de nuevo en la directiva de seguridad, sección [System Access], clave PasswordComplexity = 1. El camino es el mismo que arriba con secedit.
Dos puntos sacados de la práctica. Primero, el cambio de contraseña obligatorio cada 30 días aporta demostrablemente poco y, combinado con NLA, provoca exactamente el problema de inicio de sesión de la sección anterior. Son mejores las contraseñas largas, fijadas una sola vez y guardadas en un gestor de contraseñas. Segundo, la directiva solo vale para las contraseñas que se fijen a partir de ese momento. Una contraseña existente de seis caracteres sigue siendo válida hasta que la cambies.
Limitar el acceso a direcciones IP conocidas
Esta es la medida que acaba de verdad con el tráfico de ataque, y además por completo. Todas las reglas del grupo Escritorio remoto se pueden restringir a una lista de direcciones. Usa el identificador de grupo neutro respecto al idioma, para que el script funcione también en instalaciones en inglés:
Get-NetFirewallRule -Group '@FirewallAPI.dll,-28752' | Select-Object Name, DisplayName, Enabled, Profile
Set-NetFirewallRule -Group '@FirewallAPI.dll,-28752' -RemoteAddress @('203.0.113.10','198.51.100.0/24')
Espera más resultados de los que crees tener reglas. -Group toca todas las reglas del grupo: en un sistema de prueba eran seis, incluidas las reglas en la sombra y copias adicionales con nombre de GUID en el perfil Public. Es intencionado y es correcto, porque si no una de las copias se quedaría abierta. El listado previo con Get-NetFirewallRule te muestra de antemano qué se va a ver afectado.
Para controlar qué direcciones se han guardado realmente:
Get-NetFirewallRule -Group '@FirewallAPI.dll,-28752' | Get-NetFirewallAddressFilter
Si has cambiado el puerto, las reglas integradas ya no se aplican, porque están fijadas al 3389. Entonces necesitas una regla propia:
New-NetFirewallRule -DisplayName 'RDP restringido' -Direction Inbound -Protocol TCP -LocalPort 34567 -RemoteAddress '203.0.113.10' -Action Allow -Profile Any
El salvavidas contra tu propia regla de firewall
Quien teclee mal su dirección IP o haya introducido una dirección dinámica se deja fuera sin remedio. Crea por eso de antemano una tarea que retire la restricción por sí sola al cabo de diez minutos:
Set-Content -Path C:\rdp-rescate.ps1 -Value "Set-NetFirewallRule -Group '@FirewallAPI.dll,-28752' -RemoteAddress Any"
Register-ScheduledTask -TaskName 'RDP-Rescate' -Action (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -File C:\rdp-rescate.ps1') -Trigger (New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(10)) -User 'SYSTEM' -RunLevel Highest
Si la conexión nueva funciona, vuelves a eliminar la tarea:
Unregister-ScheduledTask -TaskName 'RDP-Rescate' -Confirm:$false
Si no tienes una dirección IP fija, la solución limpia es no exponer RDP a Internet en absoluto y llegar a él a través de una VPN. Cómo montarlo está en nuestra guía sobre el servidor VPN WireGuard. El servidor solo escucha entonces en la dirección de la VPN, y el puerto 3389 desaparece por completo de Internet.
Cambiar el puerto y lo que eso aporta realmente
Otro puerto no es una medida de seguridad, sino una medida contra el ruido. La gran masa de bots escanea únicamente el 3389 y después ya no te encuentra, lo que por lo general baja de forma drástica tu cifra de eventos 4625 y vuelve a hacer legibles los registros de eventos. Quien busca de forma dirigida encuentra el servicio igualmente: los buscadores de servicios expuestos reconocen RDP por su huella de protocolo, sea cual sea el puerto, y un escaneo completo de los 65535 puertos dura segundos.
El cambio de puerto tiene sentido, por tanto, como complemento, pero nunca como sustituto de NLA, del bloqueo de cuentas y de la restricción por direcciones. La ejecución práctica, incluida la parte que casi todas las guías hacen mal, es decir, sin reiniciar, la hemos descrito aparte: cambiar el puerto RDP sin reiniciar. Acuérdate en cualquier caso de crear una regla de firewall para el puerto nuevo antes de cambiar el servicio.
Analizar los registros de inicio de sesión
Primero tiene que estar activada la auditoría. Los nombres de subcategoría de auditpol están traducidos, así que un comando en inglés falla en un sistema en español con "Se produjo el error 0x00000057: El parámetro no es correcto." El GUID, en cambio, funciona en cualquier versión de idioma:
auditpol /set '/subcategory:{0CCE9215-69AE-11D9-BED3-505054503030}' /success:enable /failure:enable
auditpol /get '/subcategory:{0CCE9215-69AE-11D9-BED3-505054503030}'
Las comillas simples alrededor de todo el parámetro no son un capricho estético, sino obligatorias. Si no, PowerShell interpreta las llaves como un bloque de script y las elimina: de un argumento salen tres, y auditpol aborta con Se produjo el error 0x00000057: El parámetro no es correcto. y el código de salida 87, seguido del texto de ayuda. En cmd.exe la escritura sin comillas funciona, en PowerShell no. Con comillas, ambas formas terminan con código de salida 0, y la consulta de control responde entonces Inicio de sesión Correcto y error.
Bajo ataque, el registro de seguridad se llena en pocas horas y sobrescribe justo las entradas que necesitas. Dale más espacio:
wevtutil sl Security /ms:1073741824
Después, las direcciones de origen más frecuentes de la última semana, ordenadas por número. La variante que pasa por la estructura XML es la robusta, porque no depende del orden de los campos:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-7)} -ErrorAction SilentlyContinue | ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' } | Select-Object -ExpandProperty '#text' } | Group-Object | Sort-Object Count -Descending | Select-Object -First 15 Count, Name
El -ErrorAction SilentlyContinue tiene su sitio en cada una de estas llamadas. Get-WinEvent aborta con un error en rojo en cuanto no hay ni un solo evento que encaje en el periodo: "No se encontraron eventos que coincidan con los criterios de selección especificados." En un servidor recién asegurado eso es justo lo normal, así que el comando falla precisamente con el éxito que persigue esta guía.
La pregunta decisiva, sin embargo, no es quién lo ha intentado, sino si alguien lo ha conseguido. Los inicios de sesión de Escritorio remoto correctos llevan el ID de evento 4624 con el tipo de inicio de sesión 10 (RemoteInteractive):
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=(Get-Date).AddDays(-7)} -ErrorAction SilentlyContinue | Where-Object { $_.Properties[8].Value -eq 10 } | Select-Object TimeCreated, @{n='Usuario';e={$_.Properties[5].Value}}, @{n='Origen';e={$_.Properties[18].Value}} | Format-Table -AutoSize
Si ahí aparece un nombre de usuario o una dirección de origen que no puedas asignar a nadie, alguien ha encontrado una contraseña válida. Entonces ya no sirve de nada afinar las directivas: el servidor hay que reinstalarlo.
Además vale la pena echar un vistazo al registro propio de RDP. El evento 1149 nombra el usuario, el dominio y la dirección de origen de cada conexión autorizada en una sola línea:
Get-WinEvent -LogName 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' -FilterXPath '*[System[EventID=1149]]' -MaxEvents 25 | Format-List TimeCreated, Message
Renombrar el administrador o, mejor aún, desactivarlo
La cuenta de administrador integrada es el único nombre de usuario que todo atacante conoce con seguridad. Renombrarla no cuesta nada:
Rename-LocalUser -Name 'Administrator' -NewName 'kh-svc'
Ten claro, eso sí, dónde está el límite: el SID de la cuenta sigue terminando en -500, y cualquier acceso autenticado puede resolver el nombre nuevo por esa vía. El cambio de nombre funciona contra los bots masivos, no contra un atacante dirigido que ya tenga un pie dentro. Así encuentras la cuenta a pesar del nombre nuevo:
Get-LocalUser | Where-Object { $_.SID.Value -like '*-500' } | Select-Object Name, Enabled
Bastante más eficaz es desactivar del todo la cuenta integrada, una vez que tu propia cuenta de administrador de la sección "asegura la vía de vuelta" funcione de forma demostrada. Prueba el inicio de sesión con la cuenta nueva en una segunda sesión y solo entonces:
Disable-LocalUser -Name 'kh-svc'
Restringe además el acceso RDP a las personas que lo necesitan. De forma predeterminada, cualquier miembro del grupo de administradores locales puede entrar por RDP, incluidas cuentas de servicio que nunca deberían hacerlo. Esto muestra quién puede hacerlo ahora mismo:
Get-LocalGroupMember -SID 'S-1-5-32-555'
Cómo saber que ha funcionado de verdad
Comprobar en lugar de confiar. Estas cinco comprobaciones te dicen si el cambio ha surtido efecto:
- NLA:
Get-ItemPropertysobreRDP-TcpdevuelveUserAuthentication : 1ySecurityLayer : 2. Una conexión nueva pide ahora las credenciales antes de establecer la conexión, y ya no en una pantalla de inicio de sesión dentro de la ventana. - Bloqueo:
net accountsmuestra un umbral de bloqueo distinto de "Nunca". Contraprueba con una cuenta desechable: después de la undécima contraseña incorrecta, el cliente tiene que mostrar el mensaje de bloqueo, ya no el de credenciales incorrectas. - Firewall:
Get-NetFirewallAddressFiltermuestra tus direcciones en lugar deAny. Una prueba de conexión desde una dirección ajena tiene que acabar en un error de tiempo de espera, no en una petición de inicio de sesión. Compruébalo desde fuera conTest-NetConnection -ComputerName tuservidor -Port 3389, el resultado tiene que serTcpTestSucceeded : False. - Registros:
auditpol /getinforma de correcto y error para la subcategoría. - La cifra: vuelve a contar los eventos 4625 pasadas 24 horas desde el cambio. Tiene que estar bastante más baja. Si sigue alta, alguna de tus reglas no se aplica, casi siempre porque existe una segunda regla de firewall más abierta para el puerto 3389, creada por una imagen de proveedor o por la instalación de algún software. Esta línea la encuentra:
Get-NetFirewallPortFilter | Where-Object { $_.LocalPort -eq 3389 } | Get-NetFirewallRule | Select-Object DisplayName, Enabled, Profile, Action. El orden en la canalización es intencionado. Si se invierte y se mandan primero todas las reglas de firewall aGet-NetFirewallPortFilter, la llamada tarda, medido, unos 12 segundos en lugar de uno, y sobre todo falta el nombre de la regla en la salida: entonces ves cuatro resultados sin enterarte de qué reglas son.
Si de todos modos estás montando un servidor nuevo, resuelve estos puntos desde el principio en vez de añadirlos después. Para sistemas Linux vale el mismo esquema con SSH en lugar de RDP, descrito en asegurar SSH y configurar el inicio de sesión con clave, y el orden de los primeros pasos lo encuentras en nuestra lista de comprobación para servidores root nuevos.
Lo que el endurecimiento de RDP no cubre
Las medidas de arriba protegen frente a los intentos de inicio de sesión. No protegen frente a ataques volumétricos, cuyo objetivo es dejar el servidor inalcanzable a través de su conectividad. Contra eso solo ayuda el filtrado en la red que hay delante. Cómo funcionan esos ataques está en qué es un ataque DDoS, y qué precauciones siguen teniendo sentido del lado del servidor, en proteger el servidor frente a ataques DDoS. Todos los servidores de KernelHost están en el centro de datos maincubes de Fráncfort del Meno, detrás de un filtrado que intercepta el tráfico de ataque antes de que llegue al servidor.
Y no sustituyen a un backup. Un servidor en el que alguien ha iniciado sesión con éxito ya no es de fiar, por rápido que cambies la contraseña después. La única vía de vuelta fiable es un backup anterior al incidente.
Preguntas frecuentes
¿Basta con cambiar el puerto RDP del 3389 a otro puerto?
Me he dejado fuera al aplicar una regla de firewall. ¿Y ahora qué?
¿Por qué un usuario ya no puede iniciar sesión con la contraseña correcta desde que NLA está activo?
¿El bloqueo de cuentas afecta también a la cuenta de administrador integrada?
¿Puede un atacante dejarme fuera de forma permanente mediante el bloqueo de cuentas?
¿Cómo reconozco que un ataque ha tenido éxito?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

