
Endurecer un VPS nuevo en la primera hora
Una dirección IPv4 pública recibe sondeos a los pocos minutos de su primer paquete, por parte de máquinas que nunca han oído hablar de usted y nunca lo harán. Es una buena noticia: casi todo lo que apunta a un servidor nuevo es genérico, y una hora dedicada con cuidado basta para neutralizar casi todo eso. Esa misma hora, hecha en el orden equivocado, le deja fuera de una máquina en la que nadie puede iniciar sesión en su nombre.
Le entregamos una shell de root unos 47 segundos después de que se confirme el pago y, a partir de ahí, nos detenemos deliberadamente. No instalamos ningún agente, no gestionamos su firewall y no guardamos una copia de sus credenciales — una clave que nosotros tengamos es una clave que se nos puede obligar a entregar, y ese principio recorre toda la plataforma. La consecuencia es simple y merece decirse sin rodeos: la seguridad de su servidor es el estado en que lo deja durante la primera hora.
Lo que sigue es esa hora, en el orden en que la aplicamos nosotros mismos, en Debian 13 y Ubuntu 24.04. Dos ajustes hacen la mayor parte del trabajo. El resto de esta página existe por tres cosas que parecen correctas, superan la comprobación final de cualquier tutorial y están silenciosamente equivocadas: un fragmento de configuración SSH que queda anulado sin avisar, un puerto configurado que un sshd activado por socket ignora, y un motor de contenedores que publica puertos por debajo de su firewall. Las tres han perjudicado a alguien que hizo todo lo demás bien.
Qué es lo que realmente llama a la puerta
Observe journalctl -u ssh en un servidor que lleva una hora en línea, y la primera vez que lo vea el volumen le resultará alarmante. No debería serlo. Lo que está viendo es la radiación de fondo de internet: un puñado de operaciones de escaneo, algunas académicas, algunas comerciales, algunas delictivas, que enumeran todo el espacio IPv4 de forma continua y entregan los resultados a bots que prueban credenciales. Su dirección fue alcanzada porque existe, en orden numérico, y volverá a ser alcanzada dentro de unas horas haga usted lo que haga.
Esto cambia el problema de una forma útil. No se está defendiendo de un adversario que lo ha elegido a usted y que se irá adaptando; se está defendiendo de un script con un repertorio fijo — root, admin, ubuntu, test, git, oracle, postgres, y las veinte mil contraseñas que han aparecido en filtraciones de datos. No tiene paciencia, ni creatividad, ni interés alguno en una máquina que no responde al primer intento. Desactivar la autenticación por contraseña no ralentiza a este atacante: lo elimina del tablero por completo.
Vale la pena conocer dos detalles. Primero, IPv6 es muchísimo más tranquilo, porque un /64 no se puede barrer por completo — pero en el momento en que su registro AAAA se hace público, o la dirección de su máquina aparece en una cabecera de correo o en un registro de transparencia de certificados, la tranquilidad se termina. Nunca trate IPv6 como un escondite; trátelo como un pajar más pequeño. Segundo, una dirección IPv4 tiene un pasado. Tuvo un inquilino antes que usted, y si ese inquilino gestionó mal un servidor de correo o alojó algo que acabó en una lista negra, usted hereda esa reputación hasta que se disipa. Si el correo le importa, compruebe la dirección en las listas negras habituales antes de construir nada sobre ella — es una comprobación de cinco minutos que ahorra una quincena depurando problemas de entregabilidad.
Los dos ajustes que hacen el noventa por ciento del trabajo
Casi todo compromiso real de un servidor pequeño empieza en uno de dos sitios: una contraseña que se podía adivinar, o un servicio que estaba escuchando y no debería haberlo estado. Las soluciones correspondientes son PasswordAuthentication no y un firewall con una política drop por defecto. No tienen nada de vistoso, se hacen juntas en quince minutos, y valen más que todos los demás controles de esta página juntos.
La razón es estructural, no estadística. Ambas están cerradas por defecto — fallan hacia el lado seguro. Un sshd que solo acepta claves no se puede forzar por fuerza bruta por muchos intentos que lleguen, porque no existe ningún camino en el código que acepte una contraseña. Un firewall de denegación por defecto protege servicios que todavía no ha instalado, incluida la base de datos que añadirá dentro de tres meses y olvidará vincular a localhost. Todo lo demás en una lista de endurecimiento es una enumeración de cosas malas: una lista de elementos concretos que apagar, que solo es tan completa como la propia lista.
Así que si solo va a leer una sección y cerrar la pestaña, lea esta, haga los pasos 2 a 5 más abajo, y dé la hora por bien empleada. El resto es genuinamente útil y genuinamente secundario.
Dónde vive ahora la configuración de SSH, y la trampa que esconde
En Debian 13 y Ubuntu 24.04, /etc/ssh/sshd_config empieza con una línea Include /etc/ssh/sshd_config.d/*.conf, y las imágenes cloud incluyen un archivo en ese directorio — normalmente 50-cloud-init.conf — que ya establece PasswordAuthentication. Editar el archivo principal y añadir sus propias directivas al final resulta natural, pero produce una configuración que no hace lo que dice.
La regla es lo único de sshd que sorprende a casi todo el mundo: para cada palabra clave, gana el primer valor obtenido. Esto es lo contrario de casi cualquier otro sistema de fusión de configuraciones que haya usado. El Include aparece cerca del principio del archivo principal, así que un fragmento se lee antes que el cuerpo de sshd_config — y entre los fragmentos, decide el orden alfabético. Un archivo llamado 10-hardening.conf vence a 50-cloud-init.conf. Un archivo llamado 60-hardening.conf pierde frente a él, en silencio, y no se dará cuenta al reiniciar y ver que todo informa de éxito.
Por eso nunca debe fiarse del archivo que acaba de escribir. Pregúntele al daemon qué es lo que realmente interpretó:
sshd -t # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) 'sshd -T imprime la configuración efectiva una vez resueltos todos los includes, anulaciones y valores por defecto. Si esa salida dice passwordauthentication yes, la autenticación por contraseña está activada, sin importar lo que diga cualquier archivo en el disco. Nada más cuenta como verificación.
La segunda trampa es de la misma familia. Ubuntu 24.04 arranca sshd mediante activación por socket: ssh.socket es quien posee el puerto de escucha, y Port en sshd_config se ignora por completo. Compruébelo con systemctl is-enabled ssh.socket; si está habilitado y quiere un puerto distinto, cámbielo con systemctl edit ssh.socket y una anulación ListenStream= — no en sshd_config, donde el ajuste parecerá correcto y no hará nada.
Cambiar el puerto 22: teatro, pero teatro barato
Seamos honestos con este punto, porque internet no lo es. Mover sshd al puerto 2222 o al 47000 no detiene a ningún atacante competente. Un escaneo TCP completo de un host tarda segundos, el servicio se anuncia solo en el banner, y cualquiera que haya decidido atacarle a usted en concreto lo encontrará antes de terminarse el café.
Lo que sí consigue es reducir su log de autenticación en algo así como un noventa y cinco por ciento, y eso tiene valor real: es la diferencia entre unos logs que se hojean por encima y unos logs que de verdad se leen. Una señal que se puede ver gana a una señal enterrada. Si monitoriza aunque sea un poco, un auth.log tranquilo es la forma más barata de hacer visible una anomalía.
Los costes son pequeños pero reales, y son la razón por la que lo llamamos opcional en lugar de recomendado. Se olvidará del puerto cuando vuelva dentro de ocho meses. En principio, un puerto no estándar por encima de 1024 puede ser tomado por un proceso sin privilegios si en algún momento sshd deja de escuchar. Los firewalls de salida restrictivos en redes de cliente bloquean puertos poco habituales, así que de vez en cuando no podrá llegar a su propio servidor desde una oficina o un hotel. Y en un sistema con activación por socket hay que cambiarlo en el lugar correcto, según la sección anterior. Hágalo si le importan unos logs tranquilos. No lo haga y luego se sienta seguro por ello, y nunca lo haga en lugar de usar solo claves.
Denegación por defecto, y el conjunto de reglas que usamos de verdad
Un firewall que enumera qué bloquear es un archivador. Un firewall que enumera qué permitir es un control de seguridad. La diferencia lo es todo, porque solo el segundo cubre el servicio que instalará el mes que viene, el puerto de depuración que abrió un viernes, y el contenedor que decidió exponerse por su cuenta.
En nuestra plataforma dispone de dos capas, y son independientes. El filtro de borde es opcional, se configura por servidor desde el panel y se aplica en el hipervisor, de modo que el tráfico bloqueado nunca llega siquiera a su instancia — útil para reglas de capa 4 que quiere aplicar antes de que su kernel les dedique un solo ciclo, y para evitar que una instancia comprometida sea alcanzable mientras trabaja en ella. El firewall de la instancia es enteramente suyo: nftables, iptables, pf, lo que traiga su imagen. Nosotros nunca lo tocamos. Use ambos si la máquina importa; use al menos el segundo siempre.
Dos cosas del conjunto de reglas del paso 5 merecen explicación, porque la mayoría de los conjuntos de reglas copiados y pegados las hacen mal. No descarte todo el ICMP. Parece prudente, pero rompe el descubrimiento de la MTU de la ruta, lo que produce la peor clase de fallo que existe: las peticiones pequeñas funcionan, las respuestas grandes se quedan colgadas, y nada en sus logs menciona al firewall. Acepte como mínimo destination-unreachable, time-exceeded y parameter-problem. No filtre ICMPv6 por tipo a menos que conozca la lista. IPv6 depende de ICMPv6 para el descubrimiento de vecinos y los anuncios de router; bloquéelo de forma general y su conectividad IPv6 morirá de una manera que parece un problema de enrutamiento. Aceptar todo el ICMPv6 en un único host es un compromiso razonable, y el conjunto de reglas de más abajo hace exactamente eso.
Una advertencia antes de ejecutar flush ruleset: si Docker está instalado, esa línea elimina las reglas de NAT y de filtrado que escribió Docker, y la red de los contenedores deja de funcionar hasta que systemctl restart docker las restaura. Cargue primero su conjunto de reglas, reinicie Docker después, y lea la siguiente sección antes de publicar un solo puerto de contenedor.
Los puertos que no sabía que estaban abiertos
Pregúntele a la máquina en qué está escuchando. No en lo que usted cree haber instalado — en lo que está vinculado ahora mismo:
ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'Todo lo que sobrevive a ese filtro es alcanzable desde algún sitio distinto de la propia máquina. En una imagen de fábrica, lo habitual es encontrar rpcbind en el 111 (nada de lo que usted ejecute lo necesita), un exim4 escuchando, sobrante de la instalación base, y un stub de systemd-resolved que es inofensivo en loopback y un resolutor abierto si no lo está. Los peligrosos llegan después, con el software que instaló usted a propósito: PostgreSQL en 0.0.0.0:5432 porque un tutorial decía que editara listen_addresses, Redis sin contraseña porque Redis no tuvo contraseña por defecto durante la mayor parte de su vida, un nodo de Elasticsearch, un exporter de Prometheus, un notebook de Jupyter. Cada uno de ellos ha sido el primer paso de una brecha real en muchísimas ocasiones.
Y ahora, la trampa que sorprende a la gente cuidadosa. Docker no le pide permiso a su firewall. Cuando escribe -p 5432:5432, Docker inserta reglas DNAT en la cadena PREROUTING de la tabla nat, que el kernel evalúa antes de que su cadena input llegue siquiera a ver el paquete; el tráfico se reenvía entonces al contenedor. Su política de denegación por defecto en la cadena input no se consulta, ufw status muestra el puerto como cerrado, y la base de datos está en el internet público. Este es un comportamiento documentado de Docker, lleva así una década, y ha dejado expuestas una cantidad enorme de bases de datos.
La solución es una sola cadena de texto, y es el hábito que vale la pena adquirir:
# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres
# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgresPor último, consiga una opinión externa. Un escaneo de puertos desde el propio servidor le dice lo que piensa el kernel; un escaneo desde otro sitio le dice lo que ve el mundo, que es el único número que importa. Ejecute nmap -Pn -p- <your-ip> desde su portátil, no desde la máquina, y compare el resultado con la lista que esperaba.
Actualizaciones automáticas: actívelas, y en serio
La ventana que compromete a los servidores pequeños no es el zero-day. Son las cuatro semanas que pasan entre que un CVE se hace público con una prueba de concepto funcional y la siguiente vez que a usted le toca conectarse. Las herramientas de ataque absorben un nuevo fallo remoto en cuestión de días; una máquina que se parchea “cuando se tenga tiempo” queda expuesta durante todo ese intervalo, y cualquier operador honesto sabe lo largo que es en realidad ese intervalo.
La objeción a las actualizaciones automáticas es el miedo a que algo se rompa, y merece tomarse en serio — y después acotarse. Restrinja la automatización al repositorio de seguridad de su distribución, donde los mantenedores retroportan las correcciones a la versión empaquetada en lugar de publicar nuevas versiones upstream. Una actualización de seguridad de Debian para openssl es una compilación parcheada de la misma versión que ya está usando; el riesgo de que cambie el comportamiento es muchísimo menor que el riesgo de dejar un agujero remoto abierto durante un mes. Las actualizaciones de funcionalidades se quedan en manual, que es donde deben estar.
La parte que la gente se salta es el reinicio. Un libssl parcheado en el disco no le sirve de nada al proceso que cargó el antiguo al arrancar, y una actualización del kernel no hace absolutamente nada hasta que arranque con ella. O bien acepta un reinicio desatendido en una ventana horaria que elija usted, o instala needrestart y deja que le diga qué servicios siguen corriendo contra bibliotecas ya borradas — pero haga una de las dos cosas. “Las actualizaciones automáticas están activadas” mientras uptime marca 340 días es una ilusión cómoda, y es la más habitual que vemos.
fail2ban, CrowdSec, o nada en absoluto
fail2ban lee sus logs, detecta fallos repetidos desde una dirección y la bloquea durante un tiempo. CrowdSec hace lo mismo y comparte los veredictos a través de una red de participantes, de modo que puede bloquear una dirección que se ha portado mal en otro sitio antes de que llegue hasta usted. Ambos son buen software. Ninguno de los dos hace lo que la mayoría cree en un servidor SSH que solo acepta claves.
En cuanto PasswordAuthentication está en no, un ataque de fuerza bruta por SSH no puede tener éxito. No que “sea poco probable”: es que no existe ningún camino en el código para lograrlo. Bloquear una dirección tras cinco fallos, por lo tanto, no evita nada; reduce el volumen de logs y una cantidad insignificante de CPU. Eso es un beneficio real, simplemente no es un beneficio de seguridad, y la contrapartida no es gratis: una jail mal escrita que vigila el log equivocado ha dejado fuera de sus propios servidores a más administradores de los que jamás ha dejado fuera a atacantes.
Donde estas herramientas se ganan de verdad su lugar es una capa más arriba, en los servicios que sí expone de verdad: un formulario de inicio de sesión, un panel de administración de WordPress, una API con límites de tasa, un servidor de correo con SMTP AUTH. Esos sí aceptan contraseñas, sí se pueden forzar por fuerza bruta, y una lista de bloqueo es exactamente el control correcto. Así que nuestro veredicto es concreto y limitado. Sáltese la jail de SSH. Ponga CrowdSec o fail2ban delante de lo que tenga un campo de contraseña. Y elija lo que elija, ponga en la lista blanca su propia dirección de gestión primero — ignoreip existe precisamente para esa noche que, si no, recordará por los motivos equivocados.
Hacer que la máquina le avise cuando algo cambia
La prevención es lo que se puede hacer en una hora. La detección es lo que le dice si esa hora fue suficiente. No hace falta que sea elaborada, y en un servidor único, tres cosas baratas cubren la mayor parte del terreno.
Conserve los logs. En muchas imágenes el journal vive en /run y se evapora al reiniciar, lo que significa que el registro de un incidente desaparece justo en el reinicio que lo sigue. Crear /var/log/journal es una corrección de una sola línea y lo más valioso de esta sección.
Entérese de los inicios de sesión. Una línea en /etc/ssh/sshrc que dispare logger en cada inicio de sesión no cuesta nada y le da un registro limpio y fácil de buscar con grep, separado del propio ruido de sshd. Hay una salvedad que suele pillar a la gente: sshd ejecuta ~/.ssh/rc en lugar de /etc/ssh/sshrc cuando el usuario tiene uno propio, así que el archivo global del sistema se salta precisamente para la cuenta que más probablemente tenga un dotfile personalizado. Si necesita un gancho que no se pueda eclipsar así, use pam_exec en su lugar.
Sepa cómo era el sistema de archivos el primer día. AIDE registra hashes de sus binarios, bibliotecas y archivos de unit, e informa de lo que ha cambiado desde entonces. La trampa es fundamental y suele ignorarse: una base de datos almacenada en la misma máquina que vigila puede ser regenerada por quien la haya comprometido, y a partir de ahí la comprobación informará “sin cambios” para siempre. Copie la base de datos fuera de la máquina, o al menos anote su hash en otro sitio, y la herramienta pasa a valer los veinte minutos que cuesta. Dejada donde está, es solo una manta de seguridad.
Merece la pena enunciar el principio general por separado: el log del que no puede fiarse es el que está en la máquina comprometida. Todo aquello con lo que realmente cuente —un journal, una base de datos de integridad, una copia de seguridad— debería tener una copia en algún sitio que ese mismo atacante no controle. Un segundo servidor pequeño en otra jurisdicción es una respuesta legítima a esto, y uno de cinco dólares basta.
Lo que esto no hace
Todo lo anterior es trabajo de perímetro, y vale la pena precisar dónde está el límite para no confundir una puerta de entrada bien cerrada con una caja fuerte.
No protege los datos en una máquina en funcionamiento. Un kernel endurecido y un firewall cerrado son irrelevantes para cualquiera con acceso al hipervisor, y para el contenido de la RAM mientras la máquina está encendida. Si lo que le preocupa es el disco cuando el servidor está apagado, incautado o dado de baja, eso es cifrado de disco, y es un procedimiento distinto con un modo de fallo distinto — vea cifrar un disco VPS con LUKS.
No le hace anónimo. Un servidor puede estar impecablemente endurecido y aun así delatar quién es su dueño, a través de un registro WHOIS, una clave SSH reutilizada, una etiqueta de analítica, un cliente SSH que se conecta desde casa sin ningún salto intermedio, o un certificado que vincula dos identidades entre sí. Pagar con Monero y luego iniciar sesión desde su propia dirección anula el pago. Ese modo de fallo tiene su propia página: los errores que desanonimizan.
No arregla su aplicación. Una inyección SQL, un endpoint de administración sin autenticar o una dependencia con una puerta trasera no entienden de ajustes de sshd. La mayoría de los compromisos de servidores bien gestionados llegan por el puerto que usted mismo abrió, deliberadamente, a propósito, para un servicio que escribió o instaló.
No es una copia de seguridad. El ransomware, un rm -rf con una variable que resultó estar vacía, y una actualización fallida acaban todos de la misma forma. Haga una copia, guárdela en otro sitio, y restáurela una vez antes de necesitarla de verdad — la misma disciplina que hace sobrevivible una migración a otro proveedor es la que hace sobrevivible un mal martes.
Nada de esto es un argumento en contra de dedicar esa hora. Es un argumento a favor de saber exactamente qué compró esa hora.
- Despliegue con una clave, nunca con una contraseña
Genere el par de claves en su propia máquina y pegue la mitad pública en el formulario de despliegue; la imagen la instalará y nunca tendrá una contraseña de root que perder. Si despliega desde una imagen marcada como
cloud-init, puede pasar toda la configuración de la primera hora como user-data en su lugar — hasta 64 KiB — y la máquina arrancará ya endurecida.# on your laptop, not on the server ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh" cat ~/.ssh/id_ed25519.pub # paste this into the deploy formUtilice ed25519 salvo que algo en su toolchain lo rechace, en cuyo caso RSA de 4096 bits sirve perfectamente. Proteja la clave privada con una frase de contraseña y cárguela en un agente; un archivo de clave sin frase de contraseña en un portátil es como una contraseña escrita en el propio monitor. El mismo juego de claves se inyecta en el modo de rescate, que es lo que hace recuperable el paso 4.
- Abra la segunda sesión antes de tocar nada
Esto no es opcional y no es paranoia. A partir de aquí va a modificar el daemon a través del cual está conectado y el firewall que le permite llegar hasta él. Mantenga una sesión abierta e inactiva como cuerda de vuelta a la máquina, y haga cada cambio en una distinta. Si un cambio sale mal, la sesión abierta sigue autenticada y puede deshacerlo; una conexión nueva sería rechazada.
# terminal A — the rope. Log in and leave it alone. ssh -i ~/.ssh/id_ed25519 root@<server-ip> # terminal B — where every command below runs. ssh -i ~/.ssh/id_ed25519 root@<server-ip>Compruebe cada cambio abriendo una tercera conexión, nunca reconectando la que está usando para trabajar. Si la tercera conexión falla, todavía le quedan dos shells funcionando y un problema, en lugar de una crisis.
- Cree la cuenta que va a usar de verdad
Root por SSH es cómodo durante exactamente una hora. Pasada esa hora, una cuenta con nombre y sudo le da un rastro de auditoría, le protege de un comando mal escrito que se ejecute con privilegios totales, y le permite desactivar una cuenta comprometida sin desactivar la máquina.
adduser --disabled-password --gecos "" deploy install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys chown deploy:deploy /home/deploy/.ssh/authorized_keys chmod 0600 /home/deploy/.ssh/authorized_keys usermod -aG sudo deploy # a long random password, stored in your manager, used only by sudo passwd deployEstablezca esa contraseña. Una cuenta con sudo cuya contraseña nunca se estableció no puede usar sudo, y descubrir esto después de haber desactivado el login de root es la forma clásica de quedarse fuera con todos los archivos correctos. Verifíquelo antes de seguir: desde una terminal nueva,
ssh deploy@<server-ip>, y luegosudo -v. Las dos cosas deben funcionar. - Escriba el fragmento de SSH, y después pregúntele al daemon qué leyó
Un fragmento numerado que se ordene antes vence a lo que trajera la imagen cloud, según la regla de orden explicada arriba. Escríbalo, valide la sintaxis, lea la configuración efectiva resultante, y solo entonces recargue.
cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF' PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AuthenticationMethods publickey PermitEmptyPasswords no AllowUsers deploy MaxAuthTries 3 LoginGraceTime 20 X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no # remove this line if you use ssh -L / -D tunnels ClientAliveInterval 300 ClientAliveCountMax 2 EOF chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf sshd -t # silence means the syntax is valid sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) ' systemctl reload sshLea esa salida de
sshd -Tantes de recargar, no después. Debe decirpermitrootlogin noypasswordauthentication no. Si no es así, su archivo está siendo anulado por otro que se ordena antes — hagals /etc/ssh/sshd_config.d/y renombre el suyo para que quede después. Ahora abra una tercera terminal e inicie sesión comodeploy. Solo cuando eso funcione debería cerrar la cuerda de seguridad. - Cargue un firewall de denegación por defecto
nftables viene incluido en ambas distribuciones y sustituye el conjunto de reglas de iptables por un único archivo legible. Ajuste los puertos aceptados a los servicios que ejecute realmente — la lista de abajo asume SSH y un servidor web, y nada más.
apt install -y nftables cat > /etc/nftables.conf <<'EOF' #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept ct state invalid drop iif lo accept # ICMP must live: dropping it blackholes path-MTU discovery. ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept ip6 nexthdr icmpv6 accept tcp dport 22 ct state new accept tcp dport { 80, 443 } ct state new accept limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; } } EOF nft -c -f /etc/nftables.conf # check before you commit to it systemctl enable --now nftables nft list ruleset | head -40Si Docker está instalado,
flush rulesettambién elimina las reglas de Docker: ejecutesystemctl restart dockerdespués y confirme que sus contenedores siguen respondiendo. Luego, desde su portátil, ejecutenmap -Pn -p- <server-ip>y compruebe que la lista de puertos abiertos coincide con los que acaba de permitir — ni más, ni menos. - Active las actualizaciones de seguridad desatendidas
Solo el repositorio de seguridad, con una ventana de reinicio que haya elegido usted y no una que vaya posponiendo sin parar. Elija una hora tranquila para sus usuarios y que no sea en punto, para no chocar con el cron de todos los demás.
apt install -y unattended-upgrades needrestart cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF' // Debian. On Ubuntu, replace the two Origins-Pattern lines with: // "${distro_id}:${distro_codename}-security"; Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; "origin=Debian,codename=${distro_codename},label=Debian-Security"; }; Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; Unattended-Upgrade::Automatic-Reboot-Time "04:17"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; EOF unattended-upgrade --dry-run --debug | tail -25 systemctl status unattended-upgrades --no-pagerSi un reinicio desatendido es realmente inaceptable, póngalo en
"false"y deje queneedrestart -ble informe de qué servicios siguen corriendo contra bibliotecas ya borradas y si hay instalado un kernel más nuevo — y después actúe en consecuencia. Lo que no es aceptable es ninguna de las dos cosas. - Cierre lo que esté escuchando, y compruébelo desde fuera
Enumere qué está vinculado a algo que no sea loopback, elimine lo que no use, y confirme el resultado desde una máquina que no sea esta.
ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]' # the usual leftovers on a base image systemctl disable --now rpcbind.socket rpcbind 2>/dev/null apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null apt autoremove --purge -y # what each running unit is allowed to reach systemd-analyze security --no-pager | head -20Para todo lo que deba seguir en marcha pero no necesite público — una base de datos, una caché, un panel de administración, un exporter de métricas — vincúlelo a
127.0.0.1en su propia configuración y llegue hasta él mediante un túnel SSH:ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. Eso es estrictamente mejor que abrir un puerto y confiar en que la autenticación propia del servicio sea sólida, y es el patrón al que recurrir por defecto. - Registre la línea base y arme las alarmas
Diez minutos que solo dan sus frutos el día en que algo va mal — que es precisamente el día en que no podrá reconstruir nada de esto de memoria.
# persistent journal, so a reboot stops erasing the evidence mkdir -p /var/log/journal systemd-tmpfiles --create --prefix /var/log/journal systemctl restart systemd-journald # a clean, greppable line per SSH session cat > /etc/ssh/sshrc <<'EOF' logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}" EOF # file-integrity baseline — then get the database off the machine apt install -y aide aideinit mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db sha256sum /var/lib/aide/aide.db # copy this hash somewhere else last -n 20 ; lastb -n 20 # who got in, and who triedTermine anotando, fuera del servidor, cuatro cosas: la huella digital de la clave de host SSH obtenida con
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, el hash de AIDE de más arriba, los puertos que abrió deliberadamente, y dónde vive la clave privada. Esa nota es lo que convierte una mala mañana en una lista de comprobación en lugar de en una investigación.
Los controles, ordenados según lo que realmente aportan
| Control | Qué detiene | Qué no detiene | Coste | Veredicto |
|---|---|---|---|---|
| SSH solo con claves (PasswordAuthentication no) | Todos los bots que prueban credenciales en internet, de forma permanente y por construcción | A quien tenga su clave privada, o un fallo en el propio sshd | 5 minutos, una vez | Innegociable |
| Firewall de denegación por defecto (nftables) | Servicios que se le olvidó que estaban escuchando, y cualquiera que instale por accidente más adelante | Ataques contra los puertos que abrió deliberadamente | 10 minutos, una vez | Innegociable |
| Actualizaciones de seguridad desatendidas | La ventana de n días — las semanas entre un exploit público y su siguiente inicio de sesión | Los zero-days, y todo lo que necesite un reinicio que nunca programa | 5 minutos más una ventana de reinicio | Innegociable |
| Usuario con nombre y sudo en vez de root | Comandos mal escritos con privilegios totales, y procesos corriendo como root sin motivo | Una escalada de privilegios local en manos de alguien que ya está dentro | 3 minutos | Merece la pena |
| Línea base de integridad de archivos (AIDE) | Modificaciones silenciosas en binarios, bibliotecas y archivos de unit del sistema | Absolutamente nada, si la base de datos se queda en la máquina que vigila | 20 minutos más almacenamiento fuera de la máquina | Merece la pena si la máquina importa |
| Cambiar SSH del puerto 22 | Aproximadamente el 95% del volumen de su log de autenticación | A cualquiera que haga un escaneo de puertos, que es todo el que importa | 2 minutos, más la noche en que se olvide del puerto | Opcional, para logs tranquilos |
| fail2ban / CrowdSec en sshd | A los reincidentes, y la CPU que malgastan en usted | Nada, en cuanto la autenticación por contraseña está desactivada | 10 minutos, más un riesgo real de bloqueo | Sáltelo en SSH; úselo en su aplicación |
Preguntas que vale la pena responder
¿Cuánto tarda en empezar el escaneo después del despliegue?
Normalmente, minutos. Los escáneres que barren todo internet recorren el espacio IPv4 completo de forma continua, así que una dirección recién creada es alcanzada en la siguiente pasada, haga lo que haga usted en ella. Dé por hecho que le están sondeando desde el momento en que la máquina responde a su primer paquete — por eso el endurecimiento se hace en la primera hora, y no el primer fin de semana.
¿Sigo necesitando fail2ban si tengo desactivado el login por contraseña?
En SSH, no. Con PasswordAuthentication no no existe ningún camino en el código que una fuerza bruta pueda ganar, así que bloquear tras cinco fallos no evita nada — reduce el ruido en los logs, que vale algo, pero no es seguridad. Donde estas herramientas sí tienen sentido es delante de cualquier cosa que de verdad acepte una contraseña: un formulario de inicio de sesión web, un panel de administración, SMTP AUTH. Si instala una, ponga primero en la lista blanca su propia dirección.
¿ufw, firewalld o nftables?
Cualquiera de los tres, siempre que la política por defecto sea drop y usted entienda lo que ha desplegado. ufw es el más amigable, y por debajo escribe reglas nftables en el Debian y el Ubuntu actuales. nftables a secas es un único archivo legible que se puede comparar con diff y llevar a control de versiones, que es por lo que lo usamos nosotros. La elección importa mucho menos que la política por defecto — y ninguno de los tres cambia el hecho de que Docker publica puertos por debajo de los tres.
¿Debería cambiar SSH del puerto 22?
Solo para tener logs más tranquilos. No detiene a ningún atacante competente — un escaneo completo tarda segundos y el banner identifica el servicio. Sí elimina la mayor parte del ruido de su log de autenticación, lo que hace visibles las anomalías reales. Trátelo como higiene de logs, nunca como un control, y compruebe si su sshd está activado por socket antes de cambiarlo: en Ubuntu 24.04 la directiva Port de sshd_config se ignora, y el puerto pertenece a ssh.socket.
¿Las actualizaciones automáticas pueden romperme el sitio a las cuatro de la madrugada?
Restringidas al repositorio de seguridad, muy raramente. Esos paquetes son correcciones retroportadas a la versión que ya está usando, no nuevas versiones upstream. El riesgo realista es el reinicio, no el parche — así que elija usted mismo la ventana, ponga Automatic-Reboot-WithUsers en false para que espere mientras alguien tenga la sesión iniciada, y si el servicio de verdad no puede reiniciarse sin supervisión, ejecute needrestart -b con regularidad y actúe según lo que informe. La única postura indefendible es tener actualizaciones automáticas con un uptime medido en años.
Me he quedado fuera. ¿Qué opciones tengo?
Arranque el servidor en modo de rescate desde el panel. Inicia un entorno Alpine en RAM con las mismas claves SSH que su cuenta y el disco desmontado en /dev/vda, de modo que puede montarlo, arreglar el fragmento de sshd o el archivo del firewall, desmontarlo y reiniciar. El propio arranque de rescate no toca nada del disco. La huella digital de la clave de host del rescate es distinta de la habitual — eso es lo esperado, no una interceptación.
¿Es buena idea ejecutar Lynis o un script de hardening CIS en lugar de esto?
Como auditoría, sí; como sustituto, no. Lynis es una segunda opinión genuinamente útil y encontrará cosas que esta página no menciona. Los scripts automatizados de remediación CIS son otra cosa muy distinta: aplican cientos de cambios pensados para una flota de estaciones de trabajo corporativas, varios de los cuales rompen un servidor de formas difíciles de rastrear semanas después. Haga primero la hora a mano, para entender qué está haciendo su máquina, y después ejecute un auditor y lea sus hallazgos de uno en uno.
¿El endurecimiento hace que mi servidor sea anónimo?
No, y confundir ambas cosas es un error habitual y caro. El endurecimiento controla quién puede entrar; el anonimato controla quién puede saber que es suyo. Un servidor perfectamente cerrado sigue filtrando quién es su dueño a través de un registro WHOIS, una clave SSH reutilizada, una etiqueta de analítica o un login de administrador desde su dirección de casa. Pagar con Monero y luego conectarse directamente desde su propia conexión anula el pago por completo — eso se cubre en los errores que desanonimizan.
Keep exploring
Cifrar un disco VPS con LUKS
La otra mitad del trabajo: esta página cierra la red, aquella protege el disco cuando la máquina está apagada.
Errores de anonimato que delatan la identidad real
Un servidor impecablemente endurecido que aun así le dice a todo el mundo quién es su dueño. Los fallos son de comportamiento, no técnicos.
Configurar un VPS con WireGuard
El primer servicio que merece la pena poner detrás del firewall que acaba de construir — y una puerta privada a todo lo que vinculó a localhost.
Deploy your offshore server.
Elige una región. Elige un plan. Pega una clave. Paga. Los próximos 47 segundos corren de nuestra cuenta.