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.

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 escuchaSignificado
127.0.0.1, 127.0.0.53, [::1]Solo accesible desde el propio servidor
0.0.0.0Todas las direcciones IPv4
[::] o *Todas las direcciones IPv6 (y a menudo también IPv4)
Una IP concretaSolo 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

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

  • ss o netstat muestran el proceso vacío o -: ejecuta el comando con sudo. Sin privilegios solo ves los procesos de tu usuario.
  • Un puerto aparece en ss pero nmap lo marca como filtered: el cortafuegos lo bloquea, que es lo esperado para servicios internos. Si debería ser público, revisa sudo ufw status y cualquier cortafuegos externo al servidor.
  • nmap marca el puerto como closed aunque el servicio funciona en local: el servicio escucha solo en 127.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 -Pn para 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.