WireGuard entre laptop y servidor Linux: cerrar puerto SSH
Tengo una laptop de trabajo y un servidor Linux en un datacenter al que entro todos los días por SSH. Durante mucho tiempo viví con el puerto 22 abierto al público, protegido por fail2ban y un password largo. Era la receta de toda la vida y funcionaba, hasta que un martes a las tres de la mañana vi en journalctl -u sshd 4.000 intentos de login de China, Vietnam y Brasil en seis horas, y me di cuenta de que estaba jugando a la defensiva cuando el problema tiene arreglo de raíz: cerrar el puerto y abrir solo un túnel cifrado que yo controlo, con claves que se generan en mi laptop, que no dependen de ningún SaaS y que ningún script kiddie va a poder adivinar por fuerza bruta porque no están hablando con el daemon SSH en primer lugar.
Si llegaste aquí porque quieres sacar SSH de internet y reemplazarlo por un túnel punto a punto entre tu laptop y tu servidor, este es el post. Vamos a configurar WireGuard como única vía de acceso admin: cero Tailscale, cero NetBird, cero ZeroTier. Solo las herramientas que ya viven en tu kernel desde 5.6 y un binario de wg-quick que cabe en 60 KB. La receta cubre la generación de claves, la configuración del servidor y del cliente, la integración con systemd, el endurecimiento del firewall para que SSH solo escuche dentro del túnel, y los tres errores que más cuestan cuando empiezas.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 30-45 minutos para el primer túnel, 10 minutos para añadir pares adicionales después.
- Requisitos: Servidor Linux con kernel 5.6+ (Ubuntu 22.04, Debian 12, Fedora 39 o superior) y root. Laptop con Linux, macOS o Windows + WSL.
- Resultado: Puerto SSH 22 cerrado al público, acceso admin solo por túnel WireGuard (10.0.0.0/24), claves que rotan en cinco minutos.
- Troubleshooting: si el túnel no levanta, mira
wg showyjournalctl -u wg-quick@wg0— el 90% de los fallos están en el orden de las claves pública/privada o en el firewall que bloquea UDP 51820.
Por qué cerrar SSH y abrir solo WireGuard
SSH es un protocolo excelente. El problema no es SSH, es exponer el puerto 22 al internet público. Cada vez que dejas el puerto abierto, hay robots escaneando las 24 horas, listas de credenciales por defecto circulando en Telegram y campañas de credential stuffing que no distinguen entre tu servidor y el de cualquier otro. Las mitigaciones reactivas (fail2ban, port knocking, listas de IP) ayudan, pero todas comparten el mismo defecto: asumen que el puerto está abierto.
WireGuard cambia el modelo por completo. En lugar de abrir un puerto y rezar, generas un par de claves asimétricas que solo existen en tu laptop y en tu servidor. El puerto UDP 51820 queda abierto al mundo, pero nadie puede hacer nada útil con él si no tiene la clave privada correspondiente. No hay banner que probar, no hay usuario root al que intentar adivinar la contraseña, no hay CVE reciente de OpenSSH que te preocupe porque OpenSSH no está expuesto. Es el equivalente de cambiar la cerradura de tu casa y dejar de poner la llave debajo del tapete.
La diferencia con Tailscale o NetBird es operacional: aquí operas tú mismo el plano de control. No hay cuenta que se pueda suspender, no hay plan gratis que se puede volver pago, no hay coordinación entre nodos que dependa de un tercero. El costo es que tú gestionas la alta disponibilidad, el monitoreo y las claves, y eso es exactamente lo que vamos a configurar.
Decisiones de diseño antes de empezar
Antes de tocar archivos, define tres cosas que van a determinar cómo crece tu setup cuando tengas más servidores o más laptops.
Subred VPN
WireGuard usa una subred virtual sobre la que enruta los paquetes. La convención más limpia es tomar una /24 del espacio privado y dedicarla al túnel: 10.0.0.0/24 para el rango server-to-client, o 192.168.99.0/24 si prefieres algo más identificable. Lo importante es que NO choque con la LAN de tu casa, la red del datacenter donde vive tu servidor, ni ningún rango que vayas a enrutar después. El servidor VPN será 10.0.0.1 y tu laptop será 10.0.0.2; si después sumas un segundo servidor, será 10.0.0.3, y así.
Split tunnel vs full tunnel
El parámetro AllowedIPs en la sección [Peer] decide qué tráfico pasa por el túnel. Con 0.0.0.0/0 todo el tráfico de tu laptop sale por el servidor (full tunnel, útil si trabajas en Wi-Fi público). Con 10.0.0.1/32 solo el tráfico hacia el servidor va cifrado (split tunnel, tu Netflix sigue saliendo por tu ISP). Para acceso admin remoto, split tunnel es lo correcto: más rápido, menos carga en el servidor, y no rompe servicios que dependen de geolocalización.
Keepalive y NAT
Si tu laptop está detrás de un NAT hogareño (que es el 99% de los casos), el endpoint del túnel expira después de unos minutos sin tráfico. La solución es PersistentKeepalive = 25 en la sección del peer del lado de la laptop. El número significa “cada 25 segundos manda un paquete vacío para mantener viva la sesión”. 25 es el sweet spot: lo suficiente para no saturar la red, lo bastante rápido para no sufrir desconexiones.
Generar el par de claves
WireGuard usa Curve25519 para el intercambio y ChaCha20-Poly1305 para el cifrado. Cada participante tiene un par de claves: una privada (secreta, jamás sale del dispositivo) y una pública (se comparte con el peer). Genera el par en cada máquina, en el momento, sin curvas:
# En el SERVIDOR (la máquina a la que te vas a conectar)
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
# En tu LAPTOP
wg genkey | tee ~/wg_laptop_private.key | wg pubkey > ~/wg_laptop_public.key
chmod 600 ~/wg_laptop_private.key
wg genkey produce una clave privada en base64, y wg pubkey deriva la pública correspondiente. Las dos van a pipes con tee para que la clave privada quede persistida antes de calcular la pública. Los permisos 600 son obligatorios porque wg-quick rechaza archivos de clave con permisos de grupo u otros.
Ahora necesitas las dos claves públicas cruzadas: la pública de la laptop en el servidor, y la pública del servidor en la laptop. Compártelas por canal seguro (scp, age, Magic Wormhole, lo que uses; NO las mandes por correo).
Configurar el servidor (peer de la laptop)
El archivo del servidor vive en /etc/wireguard/wg0.conf. WireGuard lo lee al levantar la interfaz y se olvida; los cambios requieren un wg-quick down wg0 && wg-quick up wg0 o reiniciar el unit de systemd. La estructura canónica para un servidor que recibe conexiones de una laptop:
# /etc/wireguard/wg0.conf en el servidor
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contenido de server_private.key>
# Habilitar forwarding para que el servidor rutee tráfico que venga del túnel
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT
[Peer]
# LAPTOP
PublicKey = <contenido de wg_laptop_public.key>
AllowedIPs = 10.0.0.2/32
La sección [Interface] define quién es este nodo: su dirección dentro del túnel (10.0.0.1), el puerto UDP donde escucha (51820, puedes cambiarlo si tu ISP lo filtra), y su clave privada. PostUp y PostDown corren comandos cuando la interfaz sube y baja: aquí abrimos el forwarding para que los paquetes del túnel puedan salir al resto de la red si algún día quieres enrutar más allá del propio servidor.
La sección [Peer] describe al otro lado: su clave pública y qué dirección IP se le asigna dentro del túnel (10.0.0.2). Cuando la laptop levante su interfaz, WireGuard hace el handshake y los dos nodos tienen peer mutuamente configurados.
Configurar la laptop (peer del servidor)
En tu laptop, el archivo puede vivir en /etc/wireguard/wg0.conf (Linux con root) o ~/wg0.conf si prefieres modo de usuario con wg-quick up wg0. La estructura es la inversa:
# /etc/wireguard/wg0.conf en la LAPTOP
[Interface]
Address = 10.0.0.2/24
PrivateKey = <contenido de wg_laptop_private.key>
# Split tunnel: solo el tráfico al servidor va por el túnel
# Cambia a 0.0.0.0/0 si quieres full tunnel
# Table = 51820 permite coexistir con la tabla de ruteo principal en macOS
[Peer]
# SERVIDOR
PublicKey = <contenido de server_public.key>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25
El campo Endpoint es la IP pública del servidor (o un DNS dinámico si tu IP cambia, DuckDNS, No-IP, etc.) más el puerto UDP que configuraste en el servidor. AllowedIPs = 10.0.0.1/32 le dice al kernel de la laptop “todo lo que vaya destinado a 10.0.0.1, mandalo por el túnel”. El resto del tráfico sigue saliendo por tu Wi-Fi normal. PersistentKeepalive = 25 mantiene viva la sesión cuando estás detrás de un NAT.
Levantar las interfaces y verificar
Una vez que los dos archivos están en su sitio, levanta el túnel por ambos lados y verifica que el handshake ocurre:
# En el servidor
wg-quick up wg0
# En la laptop
wg-quick up wg0
# Verificar en cualquiera de los dos
wg show
El output de wg show muestra tres cosas que tienen que estar bien para considerar el túnel funcional: el peer con la clave pública del otro nodo tiene que aparecer listado; la línea latest handshake tiene que mostrar una fecha reciente (si dice “never”, el otro lado no llegó a conectarse); y el transfer tiene que mostrar bytes recibidos y enviados si mandas tráfico de prueba con ping 10.0.0.1 desde la laptop o ping 10.0.0.2 desde el servidor.
Si latest handshake dice “never” pero las claves públicas están bien, el problema casi siempre es el firewall: el servidor no está aceptando tráfico UDP en el puerto 51820, o el NAT de la laptop no sabe salir hacia el puerto externo. Verifica con ss -ulpn | grep 51820 en el servidor (debería mostrar wg0 escuchando) y con nc -uvz 203.0.113.10 51820 desde la laptop.
systemd para que el túnel sobreviva reinicios
wg-quick trae integración nativa con systemd. En el servidor, donde la configuración vive en /etc/wireguard/wg0.conf, habilita el unit con el sufijo @wg0:
# En el servidor
systemctl enable [email protected]
systemctl start [email protected]
systemctl status [email protected]
El unit [email protected] lee /etc/wireguard/wg0.conf, levanta la interfaz al arrancar y la baja limpiamente al apagar. Si algún día cambias una clave o agregas un peer nuevo, systemctl restart wg-quick@wg0 recarga todo sin desconectar la sesión activa más de un segundo.
En la laptop el flujo es diferente porque rara vez quieres un túnel WireGuard permanente: el unit está disponible, pero lo más práctico es levantar el túnel bajo demanda cuando salgas de casa o cuando empieces a trabajar en una red que no usas untrusted. Si quieres que suba automáticamente al iniciar sesión, systemctl --user enable [email protected] lo hace como unit de usuario.
Cerrar SSH en el firewall: solo accesible desde la VPN
Una vez que el túnel funciona, el paso clave es decirle al firewall del servidor que SSH solo escuche desde la subred WireGuard. Con nftables la receta es esta:
# /etc/nftables.d/wireguard-ssh.conf
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Lo que viene del túnel WireGuard pasa
iifname "wg0" accept
# SSH solo desde la subred VPN
tcp dport 22 ip saddr 10.0.0.0/24 accept
# DNS, HTTP, HTTPS y demás siguen su flujo normal
tcp dport { 80, 443 } accept
udp dport 53 accept
tcp dport 53 accept
# Established connections siguen funcionando
ct state established,related accept
}
}
La regla ip saddr 10.0.0.0/24 accept aplicada al puerto 22 significa que solo quien esté dentro del túnel puede hablar con SSH. El resto del tráfico al 22 se cae silenciosamente (no hay SYN-ACK, no hay banner, no hay nada que indique que hay un servidor escuchando). nft -f /etc/nftables.d/wireguard-ssh.conf aplica la regla sin reiniciar el servicio, y nft list ruleset | grep 22 confirma que quedó bien.
Si usas UFW, el equivalente es: ufw delete allow 22/tcp (quita la regla global) seguido de ufw allow from 10.0.0.0/24 to any port 22 proto tcp. Si usas firewalld, firewall-cmd --permanent --remove-port=22/tcp y firewall-cmd --permanent --zone=trusted --add-source=10.0.0.0/24. El patrón es el mismo: borra la regla global que abre SSH al público, y agrega una regla que solo permite el tráfico desde la subred VPN.
PersistentKeepalive y el truco del NAT
Cuando tu laptop está detrás de un NAT (casa, café, hotel), el router reemplaza la IP privada por la pública y mantiene un mapeo temporal. Si no hay paquetes durante 60-120 segundos, el mapeo expira y el servidor ya no puede encontrarte. PersistentKeepalive = 25 en la sección [Peer] del lado de la laptop manda un handshake vacío cada 25 segundos, manteniendo el mapeo vivo.
El costo es 1 paquete UDP de 32 bytes cada 25 segundos, lo cual es despreciable (16 bytes/segundo, no llega a MB/mes). El beneficio es que la conexión sobrevive a cambios de Wi-Fi y a NAT timeout sin que la sesión cronifique. Si necesitas desconectar para ahorrar batería cuando duermes, wg-quick down wg0 lo apaga en un segundo.
Hay un caso que suele confundir: si tu servidor está detrás de NAT (no tiene IP pública propia, vive en una LAN corporativa o en un proveedor que hace carrier-grade NAT), PersistentKeepalive tiene que ir del lado del servidor, no del lado de la laptop. La regla general: PersistentKeepalive se pone siempre del lado que está detrás de NAT.
Monitoreo y troubleshooting
WireGuard es opaco por diseño: si algo falla, no hay un log verboso que revisar. Los tres comandos que cubren el 95% de los diagnósticos son estos:
# Estado del túnel y handshake
wg show
# Logs si levantaste con systemd
journalctl -u wg-quick@wg0 -n 50 --no-pager
# Tráfico en vivo filtrado por interfaz wg0
tcpdump -ni wg0
Si wg show no muestra el peer del otro lado, el problema es de configuración (clave pública mal copiada, archivo con permisos incorrectos, wg0.conf con sintaxis rota). Si el peer aparece pero latest handshake dice “never”, el problema es de red (firewall bloqueando UDP 51820, IP pública mal escrita en Endpoint, NAT agresivo que cierra la sesión antes de que llegue el keepalive). Si el handshake ocurre pero no puedes hacer ping, el problema es de ruteo (AllowedIPs no incluye la IP destino, IP forwarding deshabilitado en el server).
Para monitoreo continuo, watch -n 5 'wg show | grep -E "peer|transfer"' te da una vista que se refresca cada cinco segundos con el estado de cada conexión. Si quieres alertas, Prometheus tiene node-exporter con textfile collector y un script bash que escribe wireguard_peers_connected 1 cada minuto a un archivo en /var/lib/node_exporter/textfile_collector/.
Resumen
El setup final es un servidor Linux con un túnel WireGuard persistente escuchando en UDP 51820, una laptop que se conecta bajo demanda con wg-quick up wg0, y un firewall que solo expone SSH a la subred 10.0.0.0/24. El puerto 22 al internet público queda cerrado, fail2ban deja de tener trabajo, y el acceso admin depende de una clave privada que solo vive en tu laptop. Cuando agregues más servidores o más laptops, copias el bloque [Peer] correspondiente y asignas la siguiente IP del rango 10.0.0.0/24. El crecimiento es lineal, no exponencial.
Preguntas frecuentes
¿Cómo verifico que el túnel está funcionando sin hacer ping?
Usa wg show en cualquiera de los dos lados. Si ves latest handshake: 12 seconds ago y transfer: 1.2 KiB received, 900 B sent, el túnel está activo. Si solo ves el peer sin handshake, hay problema de red. Si el handshake figura pero el transfer está en 0, AllowedIPs está mal configurado.
¿Qué hago si mi servidor tiene IP dinámica?
Usa un DNS dinámico (DuckDNS, No-IP, Dynu) que apunte a tu IP pública y configúralo como Endpoint = tu-host.duckdns.org:51820. WireGuard hace resolución DNS en cada handshake, así que un cambio de IP se propaga automáticamente al siguiente keepalive, máximo 25 segundos de downtime.
¿Puedo agregar una segunda laptop al mismo servidor?
Sí. Agrega un nuevo bloque [Peer] en /etc/wireguard/wg0.conf del servidor con la clave pública de la segunda laptop y una IP distinta del rango (10.0.0.3/32). En la segunda laptop, el AllowedIPs del peer del servidor sigue siendo 10.0.0.1/32. Recarga con wg-quick down wg0 && wg-quick up wg0 en el servidor.
¿Por qué no me deja levantar wg0 si la clave privada tiene permisos 644?
WireGuard rechaza claves con permisos abiertos porque cualquier usuario del sistema podría leer la clave y hacerse pasar por tu nodo. chmod 600 /etc/wireguard/wg0.conf (que contiene la clave) y chmod 600 al archivo de clave privada si lo tienes separado. El error en wg-quick up dice literalmente “Key is not allowed to be world-readable”.
¿Cuál es la diferencia práctica con Tailscale y NetBird?
Tailscale y NetBird usan WireGuard por debajo pero agregan un plano de control con coordinación de claves automático, descubrimiento de nodos y NAT traversal vía servidores DERP. La diferencia práctica es operacional: Tailscale/NetBird te quitan trabajo de gestión a cambio de depender de un servicio de terceros (o auto-hospedarlo). WireGuard pelado te da control total y cero dependencia externa, a cambio de gestionar tú mismo las claves y el keepalive.
Lee también
- Configura WireGuard en tu teléfono: accede a todo desde cualquier lugar
- WireGuard vs Tailscale vs NetBird: comparativa honesta para LATAM 2026
- Configurar Tailscale en Linux: VPN Mesh sin Abrir Puertos para Sysadmins
- Qué es NetBird: la alternativa open source a Tailscale que puedes hostear (2026)
- Protege tu SSH: Guía esencial para configurar Fail2ban en Linux
