Configurar un VPN WireGuard en tu propio servidor

Publicado el 16 min de lectura

Del servidor vacío al túnel WireGuard funcionando: pares de claves, NAT con nftables, wg-quick como servicio, código QR para el teléfono y los tres fallos en los que de verdad se atasca todo.

Qué es igual en las cuatro distribuciones y qué no

WireGuard está firmemente integrado en el kernel de Linux desde la versión 5.6. Por eso, en Debian 13, Debian 12, Ubuntu 24.04 y Ubuntu 22.04 ya no tienes que compilar un módulo DKMS, ni añadir un repositorio de backports, ni importar ninguna clave ajena. Las cuatro traen además el mismo estado de las herramientas de espacio de usuario (versión upstream 1.0.20210914), es decir: los comandos wg y wg-quick se comportan de forma idéntica en todas.

Las diferencias están únicamente en todo lo que rodea a WireGuard, y justo ahí es donde fracasan la mayoría de los tutoriales:

  • Filtro de paquetes: wireguard-tools recomienda nftables o iptables. Como apt instala la primera alternativa disponible, en una instalación Debian ligera acaba nftables, no iptables. Las líneas iptables -t nat -A POSTROUTING que se copian por todas partes se quedan ahí sin efecto mientras no instales el paquete.
  • Entrega del DNS: para la línea DNS =, wg-quick llama sin más al programa resolvconf. En Ubuntu, el paquete systemd-resolved incluye una capa de compatibilidad; en una instalación mínima de Debian a menudo no existe ningún resolvconf. Para instalarlo, el paquete adecuado se llama openresolv en Debian y en Ubuntu 22.04, mientras que en Ubuntu 24.04 ese paquete ya no existe. Esto solo afecta a los clientes Linux, no a los teléfonos.
  • Frontend de firewall: las imágenes de Ubuntu suelen traer un ufw activo, Debian por lo general no. ufw bloquea el reenvío de forma predeterminada, incluso cuando net.ipv4.ip_forward está a 1.

Comprueba primero con qué te encuentras:

apt-get update
apt-get install -y wireguard wireguard-tools nftables qrencode
wg --version
apt-cache policy wireguard-tools

Si wg --version devuelve un número de versión, el espacio de usuario está en su sitio. Si el kernel colabora no lo sabrás hasta el primer wg-quick up. Si ahí se queda el mensaje RTNETLINK answers: Operation not supported o Unable to access interface: Protocol not supported, estás ante un kernel sin soporte de WireGuard, normalmente uno muy antiguo o un kernel de contenedor muy recortado.

Generar los pares de claves sin buscarte problemas

WireGuard no conoce nombres de usuario ni certificados. Hay exactamente un par de claves por participante y, de forma opcional, una preshared key común como capa simétrica adicional. Genera ambas cosas con la umask puesta, porque si no las claves privadas quedan en el disco legibles para cualquiera:

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
wg genkey | tee /etc/wireguard/telefono.key | wg pubkey > /etc/wireguard/telefono.pub
wg genpsk > /etc/wireguard/telefono.psk
ls -l /etc/wireguard

No hace falta que crees tú el directorio /etc/wireguard, el paquete wireguard-tools ya lo trae con modo 0700. Más importante es una peculiaridad de umask: el valor solo vale dentro de la sesión de shell en la que lo defines. Si generas las claves en dos tandas, por ejemplo porque la conexión se cortó entre medias y volviste a iniciar sesión, después trabajas otra vez con la máscara predeterminada, y tanto server.key como telefono.psk acaban en el disco con 0644. Por eso, ajusta los permisos una vez más de forma explícita al terminar:

chmod 600 /etc/wireguard/*.key /etc/wireguard/*.psk

Aquí se repiten tres errores una y otra vez:

  1. Intercambiar la clave pública y la privada. Ambas son cadenas Base64 de 44 caracteres y tienen exactamente el mismo aspecto. Si en [Interface] PrivateKey acaba por descuido una clave pública, el túnel arranca igual, pero nunca se produce un handshake. Puedes deducirlo en cualquier momento: wg pubkey < /etc/wireguard/server.key tiene que dar exactamente el contenido de server.pub.
  2. Copiar también el salto de línea. Las claves copiadas con el ratón desde una terminal se traen con gusto espacios de más. wg lo responde con Key is not the correct length or format.
  3. Permisos del archivo. Si olvidas umask 077, wg-quick avisa al arrancar con Warning: `/etc/wireguard/wg0.conf' is world accessible. Eso no es cosmética: en ese archivo está la clave privada en texto plano.

La configuración del servidor

Averigua primero el nombre de tu interfaz de Internet. En máquinas virtuales se llama eth0, ens3 o enp1s0 según la imagen:

ip route show default

Después crea /etc/wireguard/wg0.conf. La red del túnel debería ser una que no te vayas a encontrar por ahí en el wifi de un hotel, así que mejor no uses 192.168.0.0/24 ni 192.168.1.0/24:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = CONTENIDO_DE_SERVER_KEY

PostUp = nft add table ip wgnat
PostUp = nft add chain ip wgnat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }'
PostUp = nft add rule ip wgnat postrouting ip saddr 10.8.0.0/24 oifname "eth0" masquerade
PostDown = nft delete table ip wgnat

[Peer]
# telefono
PublicKey = CONTENIDO_DE_TELEFONO_PUB
PresharedKey = CONTENIDO_DE_TELEFONO_PSK
AllowedIPs = 10.8.0.2/32

Sustituye eth0 por tu interfaz real. Si prefieres quedarte con iptables, instala el paquete iptables y usa en su lugar:

PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

Dos detalles que rara vez se mencionan. Primero: cada línea PostUp se ejecuta a través de una shell, y si una de ellas termina con error, wg-quick revierte el arranque completo. Una errata en la regla nft no da como resultado un túnel a medias, sino ningún túnel. Segundo: AllowedIPs tiene en el lado del servidor un significado completamente distinto que en el lado del cliente. Aquí es una tabla de asignación que dice qué dirección de origen pertenece a qué peer. Si pones la misma dirección en dos peers, gana el que se cargue en último lugar y el otro se queda mudo. Cada cliente recibe exactamente una /32.

Ajusta los permisos y comprueba la sintaxis antes de arrancar:

chmod 600 /etc/wireguard/wg0.conf
wg-quick strip wg0

wg-quick strip imprime la configuración sin las líneas propias de wg-quick. Si el comando termina bien, el archivo es formalmente correcto. Si devuelve wg-quick: Line unrecognized, lo habitual es que hayas escrito una opción en la sección equivocada, por ejemplo DNS bajo [Peer].

Activar el reenvío de IP de forma permanente

Sin reenvío, cada paquete se queda en el servidor. Lo activas de inmediato y de forma permanente:

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system
sysctl net.ipv4.ip_forward

El último comando tiene que mostrar net.ipv4.ip_forward = 1. Si quieres enviar IPv6 por el túnel, en ese mismo archivo hace falta además net.ipv6.conf.all.forwarding=1.

Si ufw está en marcha, con eso todavía no basta. ufw aplica su propia política de reenvío, que actúa con independencia del interruptor del kernel. Abre el puerto y permite el paso de forma explícita:

ufw allow 51820/udp
ufw route allow in on wg0 out on eth0

El cuadro de error típico cuando se olvida el reenvío es especialmente traicionero: el handshake funciona, el ping a 10.8.0.1 funciona, pero nada de lo que hay detrás es accesible. Quien solo mira el handshake se pasa horas buscando en el sitio equivocado.

Configurar wg-quick como servicio

El paquete trae una unidad de plantilla; el nombre que va después de la @ es el nombre del archivo de configuración sin extensión:

systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 -n 50 --no-pager

Una trampa que muchos no descubren hasta semanas después: SaveConfig = true. Esta opción escribe el estado en ejecución de vuelta al archivo cuando se detiene el servicio. En el proceso se pierden todos los comentarios, todas las líneas PostUp en su orden original y cualquier estructura que hayas metido a mano. Para un servidor cuya configuración mantienes a mano, deja esa opción fuera.

Después de arrancar no compruebes el estado por el código de salida, sino en la interfaz:

wg show
ip -brief address show wg0
ss -ulpn

ss -ulpn tiene que mostrar un proceso a la escucha en el puerto UDP 51820. wg show lista los peers, en este momento todavía sin handshake.

Configuración del cliente y código QR para el teléfono

El archivo del cliente conviene generarlo en el servidor, porque allí ya están todas las claves de todas formas. Atención: en el lado del cliente, AllowedIPs significa otra cosa, en concreto qué destinos deben pasar por el túnel. 0.0.0.0/0, ::/0 significa: todo.

mkdir -p /etc/wireguard/clients

Contenido de /etc/wireguard/clients/telefono.conf:

[Interface]
PrivateKey = CONTENIDO_DE_TELEFONO_KEY
Address = 10.8.0.2/32
DNS = 9.9.9.9, 149.112.112.112

[Peer]
PublicKey = CONTENIDO_DE_SERVER_PUB
PresharedKey = CONTENIDO_DE_TELEFONO_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = tu.servidor.direccion:51820
PersistentKeepalive = 25

PersistentKeepalive = 25 no es ningún lujo en los teléfonos. El NAT de las redes móviles suele olvidar las asignaciones UDP a los 30 o 60 segundos y, sin keepalive, el servidor ya no puede dirigirse por su cuenta a una conexión existente.

El código QR lo generas directamente en la terminal:

qrencode -t ansiutf8 < /etc/wireguard/clients/telefono.conf

En la app de WireGuard tocas el más, eliges la importación por código QR y apuntas con la cámara a la terminal. Dos consejos prácticos: reduce el tamaño de letra de la terminal antes de generar el código, porque si no, no cabe en la imagen. Y no borres el archivo justo después, lo vas a necesitar cuando cambies de dispositivo. Quien lo borre igualmente tendrá que generar un par de claves nuevo, porque la clave privada no se puede recalcular a partir de la pública.

Cómo saber que de verdad funciona

Que systemctl start vuelva sin decir nada solo significa que la interfaz existe. La prueba de verdad tiene tres niveles y conviene recorrerlos en este orden:

wg show wg0 latest-handshakes
wg show wg0 transfer

Nivel uno, el handshake. latest-handshakes imprime una marca de tiempo Unix por cada peer. Si ahí pone 0, nunca se ha llegado a establecer una conexión. En la vista detallada de wg show lees en su lugar latest handshake: 42 seconds ago.

Nivel dos, el flujo de datos. En transfer aparecen los bytes recibidos y los enviados. Solo bytes enviados y ninguno recibido significa: tus paquetes salen, pero no vuelve nada. Casi siempre es un firewall o un endpoint equivocado, nunca un problema de claves.

Nivel tres, el camino real. En el cliente compruebas si el tráfico se dirige realmente al túnel:

ip route get 1.1.1.1

Si ahí pone dev wg0, el enrutamiento es correcto. Solo después tiene sentido mirar una página que muestre tu dirección IP pública. Si muestra la dirección de tu servidor, ya has terminado.

Diagnóstico: no hay handshake

El cuadro de error más frecuente de todos. Activa primero el registro del módulo del kernel, porque suele dar la respuesta en diez segundos:

echo 'module wireguard +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w

Ahora lanza un intento de conexión desde el cliente y ve leyendo. Los tres mensajes que importan:

  • wireguard: wg0: Handshake for peer 1 (...) did not complete after 5 seconds, retrying (try 2) y nada más. Al servidor no llega ni un solo paquete. Contrástalo con tcpdump -n -i eth0 udp port 51820. Si ahí no ves nada, la culpa está en el firewall que hay delante del servidor, en el puerto, o en que el cliente se encuentra en una red que filtra el UDP saliente. Una prueba desde la red móvil en lugar de desde el wifi de la empresa separa estos casos con claridad.
  • wireguard: wg0: Invalid handshake initiation from .... Llegan paquetes, pero no encajan. Casi siempre la clave pública del servidor en el archivo del cliente no es correcta, o la preshared key solo está puesta en un lado. Una preshared key tiene que ser idéntica en ambos lados o faltar en ambos.
  • Ningún mensaje, aunque tcpdump muestre paquetes. Entonces WireGuard escucha en otro puerto o en otra dirección. Se comprueba con ss -ulpn.

Un caso especial poco documentado es la hora. WireGuard mete en el primer mensaje de handshake una marca de tiempo y el servidor recuerda por cada peer el valor más alto que ha visto nunca. Las marcas más antiguas las descarta, y esa es la protección contra la reinyección. Si un dispositivo puso su reloj muy por delante y conectó una vez, ya no vuelve a pasar después de corregir la hora. El servidor mantiene ese estado en memoria, así que lo que ayuda es: poner el reloj en hora y luego, en el servidor, wg-quick down wg0 y wg-quick up wg0. Tras volver a levantarlo, el bloqueo desaparece.

Desactiva el registro después, porque es muy hablador:

echo 'module wireguard -p' > /sys/kernel/debug/dynamic_debug/control

Diagnóstico: el DNS no funciona

Síntoma: el túnel está levantado, ping 1.1.1.1 funciona, pero no se resuelve ningún nombre. Hay exactamente tres causas.

Primera: el resolver no es accesible. Si pones DNS = 10.8.0.1, en el servidor tiene que haber realmente un servidor de nombres escuchando en esa dirección. Un Debian o un Ubuntu recién instalados no lo tienen. O bien indicas un resolver público, al que se llega a través del túnel, o bien montas uno tú mismo. Para el segundo camino basta con dnsmasq y un archivo pequeño en /etc/dnsmasq.d/wireguard.conf:

interface=wg0
bind-dynamic
no-resolv
server=9.9.9.9
server=1.1.1.1
cache-size=1000

bind-dynamic es importante porque, cuando dnsmasq arranca, es posible que wg0 todavía no exista. no-resolv es obligatorio en Ubuntu: sin esa línea, dnsmasq lee /etc/resolv.conf, encuentra allí la dirección stub 127.0.0.53 de systemd-resolved y monta un bucle. Y el punto que más muerde en Ubuntu: dnsmasq ocupa el puerto 53. En un sistema con systemd-resolved activo ese puerto ya está tomado, los dos servicios chocan y dnsmasq no arranca. Por eso las líneas interface=wg0 y bind-dynamic no son cosmética, sino que evitan que dnsmasq se ate a todas las direcciones. Comprueba después con ss -ulpn que dnsmasq y systemd-resolved no se están peleando por el puerto 53.

Segunda: el resolver queda fuera de AllowedIPs. Quien en lugar de 0.0.0.0/0 solo envía redes concretas por el túnel y pone como servidor DNS una dirección que no está en esa lista, manda las consultas por fuera del túnel, hacia la red local.

Tercera, solo en clientes Linux: falta resolvconf. La línea DNS = se la pasa wg-quick a un programa llamado resolvconf. Si no está, el arranque se interrumpe con /usr/bin/wg-quick: line 32: resolvconf: command not found. Comprueba primero si el programa existe siquiera:

command -v resolvconf

A la hora de instalarlo las distribuciones se separan, y justo en eso fallan los tutoriales copiados cuando se aplican a Ubuntu 24.04. Allí el paquete openresolv no existe ni en main ni en universe, y la llamada termina con E: Package 'openresolv' has no installation candidate:

SistemaPaquete adecuado
Debian 11, Debian 12, Debian 13apt-get install -y openresolv
Ubuntu 22.04apt-get install -y openresolv
Ubuntu 24.04apt-get install -y resolvconf

En Ubuntu 24.04, resolvconf es un paquete virtual que se resuelve sin ambigüedad a systemd-resolved y que crea de paso /usr/sbin/resolvconf, es decir, exactamente el binario que wg-quick llama para la línea de DNS. Allí puedes instalar igual de bien systemd-resolved directamente. Si quieres una línea que funcione en todos los sistemas mencionados, usa esta:

apt-get install -y openresolv || apt-get install -y resolvconf

En Ubuntu 24.04 existe una variante de esto que aparece después de una actualización desde 22.04: Failed to resolve interface "tun.wg0": No such device. La causa es un paquete resolvconf antiguo que quedó de 22.04 junto con /etc/resolvconf/interface-order, es decir, no el paquete virtual del mismo nombre de 24.04. A partir de ese archivo, wg-quick antepone tun. al nombre de la interfaz, y con eso la capa de compatibilidad de systemd-resolved no puede hacer nada. La solución es eliminar el paquete antiguo, de modo que solo quede la capa de systemd-resolved.

Diagnóstico: problemas de MTU

El cuadro de error más desagradable, porque todo parece funcionar. El handshake está, el ping va, SSH va, pero las páginas web se cargan a medias y se quedan colgadas, las descargas grandes se interrumpen y precisamente HTTPS es el afectado. El motivo: los paquetes pequeños pasan, los grandes no.

WireGuard añade 60 bytes a cada paquete cuando el túnel va sobre IPv4 (20 bytes de IP, 8 bytes de UDP, 32 bytes de WireGuard) y 80 bytes cuando va sobre IPv6. Por eso wg-quick resta de forma general 80 bytes de la MTU de ruta detectada y, en un trayecto normal de 1500, acaba en 1420. Es deliberadamente conservador y acierta en la mayoría de los casos.

No acierta cuando el camino es más estrecho que 1500, por ejemplo con DSL sobre PPPoE (1492), detrás de otro túnel o en algunas redes móviles. Mide la MTU de ruta real desde el cliente hasta la dirección pública del servidor, con el bit Don't Fragment puesto y sin túnel:

ping -M do -s 1472 -c 3 DIRECCION_DESTINO

1472 más 28 bytes de cabecera dan 1500. Si vuelve ping: local error: message too long, mtu=... o Frag needed and DF set, baja el valor paso a paso hasta que pase: 1464, 1444, 1414, 1372. Al valor que encuentres súmale 28 y réstale 80. Con 1464 serían por tanto 1492 de MTU de ruta y 1412 de MTU de túnel.

Esto se anota en la sección [Interface], en el lado que tiene el problema:

MTU = 1412

La contraprueba rápida, antes de ponerte a calcular: prueba a poner MTU = 1280. Es la MTU más pequeña que garantiza IPv6 y funciona prácticamente en todas partes. Si con eso tus páginas cargan bien, era la MTU y puedes acercarte con calma al valor óptimo. Si el problema sigue ahí, está en otro sitio.

En el propio servidor una MTU equivocada es menos habitual, pero posible: si ahí hay un valor más alto de lo que da el trayecto, solo notas el efecto hacia ciertos destinos. Un vistazo a ip -brief address show wg0 y a ip link show wg0 muestra el valor definido actualmente.

Cambiar peers en producción sin echar a todo el mundo

El reflejo de teclear systemctl restart wg-quick@wg0 después de cada cambio tira todas las conexiones existentes y reconstruye las reglas de NAT. En un servidor con varios usuarios eso es innecesariamente brusco. WireGuard sabe alinear la configuración en caliente:

wg syncconf wg0 <(wg-quick strip wg0)

El comando compara el archivo con el estado en ejecución y cambia solo las diferencias. Los peers existentes conservan su sesión. Ten en cuenta que la sustitución de procesos con <(...) requiere bash o zsh, en un sh puro falla. También puedes añadir un peer suelto directamente:

wg set wg0 peer CLAVE_PUBLICA allowed-ips 10.8.0.3/32

Ese cambio vive solo en memoria. Escríbelo también en el archivo de configuración, porque si no el peer habrá desaparecido tras el siguiente reinicio. Esa es, por cierto, la causa más frecuente de la frase "ayer todavía funcionaba".

Si quieres operar un endpoint WireGuard de forma permanente y con una dirección estable, un servidor propio es la base evidente. En KernelHost, los servidores root KVM y los servidores dedicados funcionan en el centro de datos maincubes de Fráncfort del Meno (TÜV TIER3+) dentro de nuestra propia red, PrePaid y sin permanencia mínima. Como complemento te pueden interesar nuestros artículos sobre la protección de SSH y sobre la configuración de ufw.

Preguntas frecuentes

¿Qué versión de WireGuard incluyen Debian 13, Debian 12, Ubuntu 24.04 y Ubuntu 22.04?
Las cuatro traen la misma versión upstream de las herramientas, 1.0.20210914, cada una con su revisión propia de la distribución. El módulo WireGuard en sí viene del kernel desde la versión 5.6 y no hay que compilarlo aparte. Los comandos wg y wg-quick se comportan de forma idéntica en los cuatro sistemas; las diferencias están en el filtro de paquetes (nftables o iptables), en resolvconf y en ufw. Para instalar resolvconf, el paquete se llama openresolv en Debian y en Ubuntu 22.04; en Ubuntu 24.04 ya no existe openresolv e instalas resolvconf.
¿Por qué las líneas de iptables de muchos tutoriales no funcionan en Debian?
El paquete wireguard-tools recomienda nftables o iptables como alternativa. apt instala la primera alternativa disponible, es decir, nftables. En una instalación Debian ligera, iptables no está presente en absoluto, la línea PostUp falla y eso aborta todo el arranque de wg-quick. O bien usas la variante nft, o bien instalas iptables de forma explícita.
El handshake funciona, pero no llego a Internet. ¿A qué se debe?
Comprueba en este orden: net.ipv4.ip_forward tiene que valer 1, la regla de NAT tiene que nombrar la interfaz de salida correcta (ip route show default te la muestra) y, con ufw activo, hace falta además ufw route allow in on wg0 out on eth0. El interruptor del kernel por sí solo no basta con ufw, porque ufw aplica su propia política de reenvío.
¿Cómo reconozco un problema de MTU?
Lo típico es esto: el túnel está levantado, el ping y SSH funcionan, pero las páginas web se cargan a medias y las descargas grandes se interrumpen. Pon MTU = 1280 a modo de prueba en la sección [Interface] del cliente. Si el problema desaparece, era la MTU. El valor óptimo lo determinas con ping -M do contra la dirección del servidor, bajando la carga útil hasta que pase, y sumando después 28 y restando 80.
¿Necesito un servidor DNS propio para que el DNS funcione dentro del túnel?
No. Puedes indicar sin más un resolver público en la configuración del cliente, al que se llega entonces a través del túnel. Un resolver propio en el servidor (por ejemplo dnsmasq sobre wg0) merece la pena si quieres resolver nombres internos o cachear las consultas. Lo único importante es: si pones DNS = 10.8.0.1, ahí tiene que haber realmente un servidor de nombres escuchando.
¿Por qué un dispositivo deja de pasar después de poner el reloj en hora?
WireGuard se protege de la reinyección con una marca de tiempo en el primer mensaje de handshake. El servidor recuerda por cada peer el valor más alto que ha visto y descarta los más antiguos. Si un dispositivo con el reloj adelantado al futuro llegó a conectar una vez, después de corregir la hora queda rechazado. Ese estado vive en memoria, así que ayuda un wg-quick down wg0 seguido de un wg-quick up wg0 en el servidor.

WireGuard VPN Debian Ubuntu nftables Redes Tutorial