Un puerto abierto y olvidado es la forma clásica de entrar en un servidor: una base de datos en una dirección pública, un panel de depuración, un contenedor que publicó un puerto que nadie quería publicar. Comprobarlo lleva un minuto si conoces dos comandos y una trampa. Hicimos todo lo siguiente en un servidor Ubuntu 24.04 nuevo de nuestra imagen y lo escaneamos desde una segunda máquina, así que las salidas son las que verás en tu propio servidor.
1. Qué está escuchando
El comando es ss. Las letras significan TCP, UDP, sockets en escucha, puertos numéricos y nombre del proceso:
sudo ss -tulpn
En un servidor nuevo imprime seis líneas:
Netid State Local Address:Port Process
udp UNCONN 127.0.0.54:53 systemd-resolve
udp UNCONN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 127.0.0.54:53 systemd-resolve
tcp LISTEN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 0.0.0.0:22 sshd
tcp LISTEN [::]:22 sshd
La columna que importa es Local Address. Te dice quién puede conectarse:
| Dirección | Quién llega |
|---|---|
127.0.0.1:… o 127.0.0.53 | Solo los programas de este servidor |
0.0.0.0:… | Cualquiera por IPv4, salvo que el firewall lo bloquee |
[::]:… | Cualquiera por IPv6 (y normalmente también IPv4) |
10.0.0.5:… (una dirección concreta) | Cualquiera que llegue a esa dirección |
Así que el servidor nuevo expone exactamente una cosa: SSH en el puerto 22, por IPv4 e IPv6. El resolvedor DNS en 127.0.0.53 es privado.
Para listar solo lo que Internet podría ver, filtra las direcciones de loopback:
sudo ss -tlnp | awk 'NR>1 && $4 !~ /^(127\.|\[::1\])/ {print $4, $6}'
Ejecútalo después de cada instalación. Instalamos nginx, Docker y una pequeña aplicación en Python, y la lista pasó de 2 líneas a 10, entre ellas un docker-proxy en 0.0.0.0:8081 y un socket de containerd en un puerto localhost aleatorio.
2. Qué proceso ocupa un puerto
Cuando un puerto está ocupado y quieres saber por quién:
sudo lsof -i :80 -P -n
sudo fuser 80/tcp
sudo ss -tlnp 'sport = :80'
lsof es el más legible. Para nginx mostró un proceso maestro como root y un worker como www-data, ambos en *:80, para IPv4 e IPv6. fuser imprime solo los PID, útil en scripts. Si necesitas liberar el puerto, detén el servicio en lugar de matar el PID; si no, systemd lo reinicia: sudo systemctl stop nginx.
3. Qué ve Internet
ss muestra la vista del servidor. La de Internet puede ser distinta, porque hay un firewall en medio o porque un servicio está ligado a 127.0.0.1. Para ver la realidad, escanea el servidor desde otra máquina, tu portátil o un segundo servidor. La herramienta es nmap (apt install nmap, o el instalador de nmap.org en Windows y macOS).
nmap your-server-ip
nmap -p- your-server-ip
nmap -p 22,80,443,3306 your-server-ip
La primera forma revisa los 1.000 puertos más comunes, la segunda los 65.535, la tercera una lista. En nuestro servidor nuevo, antes de cualquier firewall:
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
Nmap done: 1 IP address (1 host up) scanned in 6.83 seconds
El escaneo completo de 65.535 puertos tardó 8,9 segundos y encontró el mismo único puerto. Los puertos cerrados responden al instante con un reset, por eso escanearlos es rápido.
Importan tres estados:
- open: un programa aceptó la conexión. Es tu superficie de ataque.
- closed: el servidor respondió que allí no escucha nada. Seguro, pero confirma que el servidor existe y lo que no ejecuta.
- filtered: sin respuesta. Un firewall descartó el paquete. El escáner no puede saber si hay un servicio detrás.
Para un solo puerto sin instalar nada, nc -zv ip-del-servidor 443 responde succeeded o refused, y se queda esperando hasta el tiempo límite en un puerto filtrado. Para UDP, añade -sU a nmap y espera lentitud; en nuestro servidor los tres puertos UDP habituales, 53, 123 y 161, estaban closed.
Una nota para los planes NAT: desde Internet solo llega el puerto SSH que te redirigimos. Un escaneo de la dirección compartida muestra nuestro borde, no tu servidor, y los demás puertos son inaccesibles, escuche lo que escuche en ellos. En un plan con su propia IPv4, cada puerto abierto lo gestionas tú.
4. Cierra lo que no necesitas
Tres formas, de la mejor a la más amplia.
Ligarlo a localhost. Si solo usan un servicio los programas del servidor, haz que escuche en 127.0.0.1. Iniciamos una pequeña aplicación web con --bind 127.0.0.1 8080: ss la mostró en 127.0.0.1:8080 y nmap desde fuera informó 8080/tcp closed. Para bases de datos es el valor por defecto correcto: Redis, MySQL y PostgreSQL escuchan en localhost de serie en Ubuntu, y nuestras guías de Redis y MySQL lo dejan así.
Detener el servicio. sudo systemctl disable --now nombre quita el listener para siempre. Comprueba con ss que ha desaparecido.
Activar el firewall. Permite primero SSH, luego lo que sirves, y después actívalo:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw enable
sudo ufw status verbose
El escaneo desde fuera cambió por completo:
Not shown: 997 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8081/tcp open blackice-icecap
Nmap done: 1 IP address (1 host up) scanned in 10.18 seconds
Cada puerto no permitido pasó de closed a filtered, y el escaneo completo de 65.535 puertos tardó 110 segundos en vez de 8,9. Más tiempo por puerto para el escáner, menos información sobre tu servidor. Nuestra guía de UFW cubre límites de frecuencia, permitir una sola IP e IPv6.
Pero mira la tercera línea.
5. La sorpresa de Docker
El puerto 8081 siguió abierto con el firewall activo y sin regla que lo permitiera. Ese puerto pertenece a un contenedor que iniciamos con docker run -p 8081:80 nginx:alpine. Docker añade sus propias reglas al firewall del kernel, y se evalúan antes que las de ufw. La regla que escribió en nuestro servidor:
-A DOCKER ! -i docker0 -p tcp --dport 8081 -j DNAT --to-destination 172.17.0.2:80
Cualquier paquete al puerto 8081 se reenvía al contenedor antes de que ufw tenga voz. Lo hace cada contenedor con -p o una entrada en ports:, y ss lo muestra con honestidad como docker-proxy en 0.0.0.0:8081.
La solución es publicar solo en localhost:
docker rm -f web
docker run -d --name web -p 127.0.0.1:8081:80 nginx:alpine
En Compose, escribe "127.0.0.1:8081:80" en ports:. Tras el cambio, ss mostró 127.0.0.1:8081, la regla de Docker ganó -d 127.0.0.1/32 y el escaneo desde fuera informó 8081/tcp filtered. Pon nginx como proxy inverso delante para la parte pública, con HTTPS, de modo que los únicos puertos abiertos del servidor sean 22, 80 y 443.
Lista de comprobación tras cada cambio
sudo ss -tulpn: ¿cada línea con0.0.0.0o[::]es algo que querías exponer?nmap ip-del-servidordesde tu portátil: ¿coinciden los puertos abiertos?- Una base de datos, caché o panel de administración en una dirección pública: muévelo hoy a
127.0.0.1o detrás del firewall. - Cada entrada
ports:de Docker: ¿empieza por127.0.0.1:si no es el puerto web público? - SSH expuesto a todos: añade fail2ban o acceso solo con clave.
En Windows Server, la misma comprobación es Get-NetTCPConnection -State Listen, y las reglas viven en el Firewall de Windows. Cada plan Linux, desde el Nano de $3, tiene una versión con su propia dirección IPv4, y ahí lo que ves en ss es lo que ve el mundo, menos tu firewall.
Comentarios
Aún no hay comentarios. Sé el primero.