Cada puerto abierto en un servidor es un servicio expuesto que puede ser atacado, así que conviene saber exactamente cuáles hay y por qué. Para eso se combinan dos puntos de vista: desde dentro, ss (o el clásico netstat) muestra qué procesos escuchan en qué puertos y direcciones; desde fuera, nmap muestra qué llega realmente a través de la red y el cortafuegos. En este tutorial auditarás los puertos de un servidor Ubuntu 24.04 con ambas herramientas, cerrarás los que no deban estar expuestos y programarás una comprobación que avise cuando aparezca un puerto nuevo.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios
sudo. - Una segunda máquina (tu ordenador u otro servidor) desde la que escanear con
nmap. Escanear desde el propio servidor no sirve para ver qué deja pasar el cortafuegos. - La IP pública del servidor, a la que llamaremos
your_server_ip.
Importanteescanea solo sistemas que sean tuyos o para los que tengas autorización por escrito. Escanear equipos ajenos puede ser ilegal y suele infringir las condiciones de uso de los proveedores.
Paso 1: Ver los puertos en escucha con ss
ss forma parte del paquete iproute2, instalado por defecto en Ubuntu, y es el sustituto moderno de netstat. Para listar los sockets TCP y UDP en escucha con el proceso que los abre:
sudo ss -tulpn
Las opciones significan: -t TCP, -u UDP, -l solo en escucha, -p mostrar el proceso (requiere sudo para ver procesos de otros usuarios) y -n no resolver nombres de puertos ni direcciones.
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=16))
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=612,fd=14))
tcp LISTEN 0 4096 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=1402,fd=6))
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1021,fd=3),("systemd",pid=1,fd=112))
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1188,fd=6))
tcp LISTEN 0 151 0.0.0.0:3306 0.0.0.0:* users:(("mysqld",pid=1290,fd=21))
La columna clave es Local Address:Port:
| Dirección de escucha | Significado |
|---|---|
127.0.0.1, 127.0.0.53, [::1] | Solo accesible desde el propio servidor |
0.0.0.0 | Todas las direcciones IPv4 |
[::] o * | Todas las direcciones IPv6 (y a menudo también IPv4) |
| Una IP concreta | Solo en esa interfaz |
En el ejemplo, Redis y el resolvedor DNS local solo escuchan en localhost, pero MySQL (3306) escucha en todas las interfaces. Si ninguna máquina externa necesita conectarse a MySQL, es un puerto que sobra.
Algunos filtros útiles:
sudo ss -tlnp 'sport = :80' # quién escucha en el puerto 80
ss -tn state established # conexiones TCP activas
ss -s # resumen por tipo y estado
Notaen Ubuntu 24.04 SSH usa activación por socket, por eso el puerto 22 aparece asociado tanto a
sshdcomo asystemd(PID 1).
Paso 2: Obtener la misma información con netstat y lsof
netstat sigue siendo habitual en documentación y scripts antiguos. No viene instalado en Ubuntu 24.04; está en el paquete net-tools:
sudo apt update
sudo apt install net-tools
Las opciones son las mismas que en ss:
sudo netstat -tulpn
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1/init
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1188/nginx: master
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 1290/mysqld
tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 1402/redis-server 1
udp 0 0 127.0.0.53:53 0.0.0.0:* 612/systemd-resolve
Para investigar un puerto concreto, lsof (instalado por defecto) muestra el proceso, el usuario y el descriptor:
sudo lsof -nP -i :3306
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 1290 mysql 21u IPv4 23456 0t0 TCP *:3306 (LISTEN)
Con el PID puedes averiguar a qué servicio de systemd pertenece el proceso, lo que te dirá cómo gestionarlo:
systemctl status 1290
Paso 3: Escanear el servidor desde fuera con nmap
Lo que escucha un proceso no es lo mismo que lo que está expuesto: el cortafuegos puede bloquear un puerto que un servicio escucha en 0.0.0.0. Para ver la exposición real, escanea desde tu segunda máquina. En Ubuntu o Debian:
sudo apt install nmap
En macOS puedes instalarlo con brew install nmap y en Windows con el instalador oficial de nmap.org.
Empieza con un escaneo de los 1000 puertos TCP más habituales:
nmap your_server_ip
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for your_server_ip
Host is up (0.012s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3306/tcp open mysql
Nmap done: 1 IP address (1 host up) scanned in 5.21 seconds
Los estados que verás:
open: un servicio acepta conexiones. Es lo que debes justificar puerto a puerto.closed: el puerto responde pero no hay servicio. No es un riesgo, pero indica que el cortafuegos no lo filtra.filtered: no hay respuesta, normalmente porque un cortafuegos descarta los paquetes.
Los servicios pueden escuchar en cualquier puerto, así que escanea también el rango completo. Con sudo, nmap usa un escaneo SYN (-sS), más rápido que el de conexión completa (-sT) que usa sin privilegios:
sudo nmap -sS -p- your_server_ip
Para identificar qué software y versión hay detrás de cada puerto abierto, usa la detección de servicios en los puertos encontrados:
nmap -sV -p 22,80,3306 your_server_ip
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.24.0 (Ubuntu)
3306/tcp open mysql MySQL 8.0.39-0ubuntu0.24.04.2
No olvides UDP, donde suelen quedar expuestos servicios como DNS, NTP o SNMP. El escaneo UDP es lento, así que limítalo a los puertos más comunes:
sudo nmap -sU --top-ports 50 your_server_ip
Si tu servidor tiene IPv6, escanéalo también con -6: es habitual que el cortafuegos IPv4 esté bien configurado y el IPv6 no.
sudo nmap -6 -sS -p- your_server_ipv6
Guarda los resultados en un archivo para compararlos en el futuro. -oN escribe el formato normal legible:
sudo nmap -sS -p- -oN escaneo-$(date +%F).txt your_server_ip
Paso 4: Cerrar los puertos que sobran
Con la lista de ss y la de nmap, decide para cada puerto abierto una de estas tres opciones, de más a menos segura.
1. Desactivar el servicio si no lo necesitas. Por ejemplo, si MySQL se instaló como dependencia y no se usa:
sudo systemctl disable --now mysql
2. Hacer que escuche solo en localhost si solo lo usan aplicaciones del propio servidor. En MySQL, edita la directiva bind-address:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 127.0.0.1
Reinicia el servicio y comprueba que ya no escucha en todas las interfaces:
sudo systemctl restart mysql
sudo ss -tlnp 'sport = :3306'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=2210,fd=23))
3. Filtrarlo con el cortafuegos si debe estar accesible solo desde ciertas IP. Con UFW, permite primero SSH para no perder el acceso, después los servicios públicos y, por último, el acceso restringido:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 203.0.113.10 to any port 3306 proto tcp
sudo ufw enable
sudo ufw status numbered
Sustituye 203.0.113.10 por la IP que necesita acceder. UFW deniega por defecto todo el tráfico entrante no permitido.
Vuelve a escanear desde fuera para confirmar el resultado. El 443 aparece como closed porque UFW lo permite pero todavía no hay ningún servicio HTTPS escuchando:
nmap -p 22,80,443,3306 your_server_ip
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp closed https
3306/tcp filtered mysql
Paso 5: Detectar puertos nuevos automáticamente
Un paquete recién instalado o un contenedor mal configurado pueden abrir un puerto sin que te des cuenta. Una comprobación periódica que compare los puertos en escucha con una lista de referencia lo detecta pronto.
Guarda la situación actual, ya revisada, como referencia:
sudo install -d -m 0750 /var/lib/port-baseline
sudo ss -Hltnu | awk '{print $1, $5}' | sort -u | sudo tee /var/lib/port-baseline/baseline.txt
tcp 0.0.0.0:22
tcp 0.0.0.0:80
tcp 127.0.0.1:3306
tcp 127.0.0.1:6379
udp 127.0.0.53%lo:53
udp 127.0.0.54:53
Crea el script de comprobación:
sudo nano /usr/local/sbin/port-check
#!/usr/bin/env bash
set -euo pipefail
baseline=/var/lib/port-baseline/baseline.txt
if [[ ! -f "$baseline" ]]; then
echo "No existe $baseline" >&2
exit 2
fi
current=$(ss -Hltnu | awk '{print $1, $5}' | sort -u)
if ! changes=$(diff "$baseline" <(printf '%s\n' "$current")); then
logger -t port-check -p auth.warning "Cambios en los puertos en escucha: $changes"
printf '%s\n' "$changes"
exit 1
fi
El script compara el protocolo y la dirección de escucha, sin el PID, para que un reinicio del servicio no genere avisos. Si hay diferencias, las escribe en el journal con la etiqueta port-check y termina con error. Hazlo ejecutable y pruébalo:
sudo chmod 0755 /usr/local/sbin/port-check
sudo /usr/local/sbin/port-check && echo "Sin cambios"
Sin cambios
Prográmalo cada hora con un timer de systemd. Crea el servicio:
sudo nano /etc/systemd/system/port-check.service
[Unit]
Description=Comprobar cambios en los puertos en escucha
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/port-check
Y el timer:
sudo nano /etc/systemd/system/port-check.timer
[Unit]
Description=Comprobar puertos en escucha cada hora
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
Actívalo y comprueba cuándo se ejecutará:
sudo systemctl daemon-reload
sudo systemctl enable --now port-check.timer
systemctl list-timers port-check.timer
Para probar que detecta cambios, abre un puerto temporal con Python en otra terminal, ejecuta el servicio y revisa el journal:
python3 -m http.server 8081
sudo systemctl start port-check.service
journalctl -t port-check -n 5 --no-pager
servidor01 port-check[3120]: Cambios en los puertos en escucha: 3a4
> tcp 0.0.0.0:8081
Detén el servidor de Python con Ctrl+C. Cuando hagas un cambio legítimo, por ejemplo instalar un servicio nuevo, vuelve a generar la referencia con el mismo comando del principio de este paso.
Solución de problemas
ssonetstatmuestran el proceso vacío o-: ejecuta el comando consudo. Sin privilegios solo ves los procesos de tu usuario.- Un puerto aparece en
sspero nmap lo marca comofiltered: el cortafuegos lo bloquea, que es lo esperado para servicios internos. Si debería ser público, revisasudo ufw statusy cualquier cortafuegos externo al servidor. - nmap marca el puerto como
closedaunque el servicio funciona en local: el servicio escucha solo en127.0.0.1. Revisa su directiva de escucha (bind-address,listen, etc.). - nmap tarda mucho o no ve el host: algunos cortafuegos descartan el ping que nmap usa para saber si el host está activo. Añade
-Pnpara escanear igualmente. - Un puerto publicado por Docker está accesible aunque UFW lo bloquea: Docker inserta sus propias reglas de iptables antes que UFW. Publica el puerto solo en localhost (
-p 127.0.0.1:8080:80) si no debe ser público.
Conclusión
Has listado los puertos en escucha con ss, netstat y lsof, comprobado la exposición real desde fuera con nmap en TCP, UDP e IPv6, cerrado los puertos que sobraban desactivando servicios, limitando su dirección de escucha o filtrándolos con UFW, y programado una comprobación que avisa cuando aparece un puerto nuevo. Como siguientes pasos, puedes repetir el escaneo externo tras cada cambio importante, reforzar la configuración de SSH y añadir Fail2ban para bloquear intentos de acceso a los servicios que sí deben estar expuestos.
