Hay un problema que llevas arrastrando desde 06-03 sin resolverlo del todo. PostgreSQL escucha en 10.0.2.15:5432 y ufw solo lo permite desde 10.0.2.0/24, lo cual está bien mientras estés dentro de esa red. El panel de estadísticas de HAProxy de 07-07 tiene la misma restricción. Y cuando Marta necesita mirar un informe desde casa, o cuando tú tienes que diagnosticar algo un domingo, la única vía es SSH al servidor y trabajar desde allí — que funciona, pero no escala y no sirve para una herramienta gráfica.

La tentación evidente es abrir el 5432 al mundo con una contraseña fuerte. Es exactamente lo que no se hace: expondrías el motor de base de datos a los escáneres automáticos que recorren Internet, y el día que aparezca una vulnerabilidad de autenticación en PostgreSQL —han aparecido— tu base de datos entera estaría en juego mientras aplicas el parche.

Esta lección monta la alternativa correcta: una VPN con WireGuard. Al terminar, la red interna será alcanzable desde cualquier sitio con un solo puerto UDP abierto, PostgreSQL y el panel de estadísticas quedarán cerrados al exterior, y tendrás el mismo mecanismo listo para llegar al servidor de medios de 08-03 desde un hotel.

Contenido

  1. Qué resuelve una VPN y qué no
  2. Casos de uso y cuál necesita Tramontana
  3. WireGuard frente a OpenVPN e IPsec
  4. Cómo funciona WireGuard: interfaz, pares y cryptokey routing
  5. AllowedIPs: enrutamiento y control de acceso a la vez
  6. Instalación y generación de claves
  7. Configuración del servidor
  8. Configuración de los clientes: portátil y móvil
  9. Enrutamiento, NAT y túnel dividido frente a túnel completo
  10. DNS dentro del túnel
  11. Integración con el cortafuegos y cierre de puertos
  12. Verificación
  13. Gestión de clientes: alta, baja y rotación
  14. Seguridad: lo que protege y lo que no
  15. Automatización con Ansible
  16. Operación y problemas habituales

Qué resuelve una VPN y qué no

Una red privada virtual crea un túnel cifrado entre dos puntos de forma que el tráfico que circula por dentro es indescifrable e inalterable para quien lo observe desde fuera, y los extremos aparecen como si estuvieran en la misma red local.

Eso es todo lo que hace. Y conviene enunciarlo con precisión porque el marketing de las VPN comerciales ha creado una nube de expectativas falsas:

Una VPN sí Una VPN no
Cifra el tráfico entre el cliente y el servidor VPN Te hace anónimo
Permite alcanzar servicios internos sin publicarlos Protege de un sitio web malicioso
Autentica al cliente antes de darle acceso a la red Protege de malware en tu equipo
Oculta el contenido en una red Wi-Fi pública Cifra más allá del servidor VPN
Reduce a uno los puertos que hay que exponer Convierte una red interna en segura
Permite políticas de acceso por origen Sustituye a la autenticación de cada servicio

Las dos filas más importantes de la derecha:

«No cifra más allá del servidor VPN.» El tráfico va cifrado del portátil de Marta al servidor. A partir de ahí sale como cualquier otro tráfico. Si va a PostgreSQL sin TLS, viaja en claro por la red interna. La VPN traslada el punto de confianza, no lo elimina — y por eso en 08-02 configuraste hostssl para la replicación.

«No convierte una red interna en segura.» Es el error conceptual con peores consecuencias, y volveremos a él en el apartado 14.

Casos de uso y cuál necesita Tramontana

Caso Topología Quién inicia Ejemplo
Acceso remoto Cliente → red interna Personas Marta consulta el panel desde casa
Unir dos sedes Red ↔ red Los routers, permanente Oficina y almacén como una sola red
Salida por otra IP Cliente → Internet Personas Ver contenido de otro país; ocultar la IP
Acceso remoto Dos sedes Salida por otra IP
Túnel Dividido normalmente Dividido siempre Completo
Quién es «cliente» Un dispositivo Toda una red Un dispositivo
Persistente No: se conecta al usarlo Sí, siempre Mientras se use
NAT necesario Solo si túnel completo No Sí
Riesgo principal Un dispositivo comprometido Una sede compromete la otra Confiar en el proveedor

Tramontana necesita el primero: acceso remoto. El objetivo concreto y acotado:

  • Que Marta llegue al panel de estadísticas de HAProxy y a los informes desde casa.
  • Que tú llegues a PostgreSQL en 10.0.2.15:5432 con una herramienta gráfica, sin exponerlo.
  • Que el acceso SSH deje de depender de tener el puerto 22 publicado en Internet.
  • Que, en consecuencia, se puedan cerrar 5432 y 8404 al exterior.

Lo que no hace falta: unir sedes (solo hay una), ni salida por otra IP (nadie lo necesita, y montarlo obligaría a enrutar todo el tráfico personal de Marta por el servidor de la empresa, lo cual además tiene implicaciones de privacidad que no queremos).

WireGuard frente a OpenVPN e IPsec

WireGuard OpenVPN IPsec (strongSwan)
Líneas de código ~4.000 ~100.000 ~400.000 (+ IKE)
Dónde corre En el kernel Espacio de usuario Kernel + demonio IKE
Rendimiento típico Muy alto, cerca del cable Moderado Alto
Criptografía Fija, sin opciones: ChaCha20, Poly1305, Curve25519, BLAKE2s Configurable (y mal configurable) Muy configurable
Configuración Un fichero de 15 líneas Decenas de directivas + PKI Compleja
Transporte Solo UDP UDP o TCP (puede pasar por proxies) UDP 500/4500 + ESP
Atraviesa NAT Sí, muy bien: keepalive Sí Con NAT-T, a veces problemático
Cambio de red sin cortar (móvil) Sí, transparente Reconexión visible Con MOBIKE
Autenticación Claves públicas por par Certificados, usuario/contraseña, TOTP Certificados, PSK, EAP
Usuario y contraseña, MFA No de serie Sí Sí
Auditabilidad Alta: se puede leer entero Baja por tamaño Muy baja
Estándar del sector Creciente, en el kernel Linux Muy extendido El de los equipos de red

La elección es WireGuard, y las razones en orden de peso:

  1. Las 4.000 líneas de código no son una curiosidad: son la razón de que se pueda auditar de verdad y de que la superficie de ataque sea minúscula. OpenVPN e IPsec han tenido vulnerabilidades graves; con veinticinco veces menos código, hay veinticinco veces menos sitios donde esconderlas.
  2. La criptografía no es configurable, y eso es una ventaja. No hay negociación de algoritmos, no hay suites débiles que desactivar, no hay ataques de degradación. La mayoría de las VPN inseguras del mundo no lo son por un fallo del software, sino por una configuración mal hecha; WireGuard elimina esa categoría entera de errores.
  3. Corre en el kernel, sin copiar cada paquete a espacio de usuario. En un servidor de 2 vCPU, la diferencia se nota.
  4. La configuración cabe en una pantalla y se entiende entera, lo que hace que se pueda revisar.

Y las dos razones honestas para elegir otra cosa:

  • Si necesitas usuario, contraseña y segundo factor, WireGuard no lo hace: autentica por clave pública, punto. Existen capas por encima, pero no es nativo. En una empresa con cien empleados y rotación, OpenVPN con LDAP y TOTP puede ser mejor.
  • Si la red de destino bloquea UDP —algunas redes corporativas y de hoteles solo dejan salir 80 y 443 por TCP—, WireGuard sencillamente no conectará. OpenVPN sobre TCP/443 pasa por casi cualquier sitio disfrazado de HTTPS.

Para Tramontana, con dos o tres personas de confianza y control sobre los dispositivos, WireGuard es claramente la opción correcta.

Cómo funciona WireGuard: interfaz, pares y cryptokey routing

Aquí están los tres conceptos que hacen que WireGuard se entienda o no se entienda. Merecen leerse despacio.

  1. Es una interfaz de red, no un servicio

OpenVPN es un demonio que corre, se configura, arranca, para y mantiene conexiones. WireGuard no es un demonio: es un tipo de interfaz de red del kernel, como eth0 o como una interfaz VLAN.

$ ip -brief link show
lo            UNKNOWN  00:00:00:00:00:00
enp0s3        UP       08:00:27:a1:b2:c3
wg0           UNKNOWN  # <- no tiene direccion MAC: es de capa 3

Las consecuencias prácticas son grandes:

  • Se configura con ip y con wg, las herramientas de red de siempre.
  • El enrutamiento es el enrutamiento normal de Linux: ip route, tablas, métricas. Lo de 06-01 se aplica tal cual.
  • El cortafuegos la trata como cualquier otra interfaz: las reglas de nftables de 06-03 funcionan igual.
  • No hay «servidor» ni «cliente» en el protocolo. Ambos extremos son pares (peers) idénticos. Lo que llamamos servidor es simplemente el par que tiene IP pública fija y no especifica Endpoint de los demás.

  1. Un par de claves por dispositivo, y nada más

No hay autoridad certificadora, ni certificados, ni listas de revocación, ni PKI. Cada dispositivo genera un par de claves Curve25519. Para que dos dispositivos hablen, cada uno necesita la clave pública del otro. Eso es toda la gestión de identidad.

Elemento Dónde vive Se comparte
Clave privada Solo en el dispositivo que la generó Nunca
Clave pública En el dispositivo y en la configuración del otro par Sí, libremente
Clave precompartida (opcional) En ambos extremos Solo entre esos dos

  1. Cryptokey routing: el concepto central

Aquí está la idea que hace a WireGuard distinto de todo lo demás. En una VPN clásica hay dos mecanismos separados: uno decide por dónde va un paquete (enrutamiento) y otro decide si se permite (control de acceso). WireGuard los fusiona en uno.

Cada par tiene asociada una lista de direcciones IP: AllowedIPs. Y esa lista se usa en las dos direcciones, con dos significados distintos:

graph LR
    subgraph SALIDA["Paquete SALIENTE (¿a qué par lo mando?)"]
        A["Paquete con<br/>destino 10.8.0.3"] --> B{"Buscar en las tablas<br/>AllowedIPs de los pares"}
        B -->|"coincide con<br/>peer luis"| C["Cifrar con la clave<br/>pública de luis<br/>y enviar a su Endpoint"]
        B -->|"no coincide<br/>con ninguno"| D["DESCARTAR<br/>(no hay ruta)"]
    end
    subgraph ENTRADA["Paquete ENTRANTE (¿lo acepto?)"]
        E["Paquete cifrado<br/>con la clave de luis"] --> F["Descifrar y mirar<br/>la IP de ORIGEN"]
        F --> G{"¿La IP de origen está<br/>en los AllowedIPs de luis?"}
        G -->|Sí| H["Aceptar y entregar<br/>al sistema"]
        G -->|"No"| I["DESCARTAR<br/>(suplantación)"]
    end

Leído en palabras:

  • Al enviar, AllowedIPs funciona como una tabla de rutas: el paquete se cifra con la clave del par cuya lista contenga la IP de destino.
  • Al recibir, AllowedIPs funciona como un filtro antisuplantación: un paquete descifrado correctamente con la clave de luis cuya IP de origen no esté en los AllowedIPs de luis se descarta.

Esa segunda parte es lo que hace a WireGuard seguro por diseño. Aunque el portátil de Luis esté comprometido y su clave privada robada, el atacante no puede enviar paquetes fingiendo ser 10.8.0.99 ni 10.0.2.15: la criptografía y el enrutamiento están atados, y el kernel descarta el paquete antes de que llegue a ninguna parte.

  1. No hay estado de conexión

WireGuard no tiene «conectar» ni «desconectar». La interfaz existe o no existe. Cuando hay un paquete que enviar, WireGuard hace un saludo de un viaje de ida y vuelta —si no lo ha hecho en los últimos dos minutos— y lo envía. Si no hay tráfico, no hay paquetes.

Esto tiene tres consecuencias muy prácticas:

  • Cambiar de red no corta nada. Un móvil que pasa de Wi-Fi a 5G sigue funcionando: WireGuard detecta el cambio de origen en el siguiente paquete válido y actualiza el extremo automáticamente (roaming).
  • Un par inactivo es indistinguible de uno apagado. wg show indica cuándo fue el último saludo, no si está «conectado».
  • WireGuard es silencioso ante quien no tiene claves. Un paquete que no se descifra correctamente se descarta sin responder nada. Para un escáner de puertos, el puerto UDP de WireGuard es indistinguible de un puerto cerrado. Esa propiedad, por sí sola, ya justifica preferirlo a exponer servicios.

Instalación y generación de claves

$ sudo apt install wireguard wireguard-tools qrencode
$ modinfo wireguard | head -3
filename:       /lib/modules/6.8.0-41-generic/kernel/drivers/net/wireguard/wireguard.ko.zst
license:        GPL v2
description:    WireGuard secure network tunnel

El módulo está en el kernel desde la versión 5.6, así que en Ubuntu 24.04 con el kernel 6.8 no hay nada que compilar.

Generar las claves con los permisos correctos

# umask 077 ANTES de generar: si no, la clave privada nace con
# permisos 644 durante un instante, y ese instante basta.
$ umask 077
$ sudo mkdir -p /etc/wireguard/claves
$ cd /etc/wireguard

# Servidor
$ wg genkey | sudo tee claves/servidor.key | wg pubkey | sudo tee claves/servidor.pub
kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=

# Clave precompartida por cliente (apartado 14)
$ wg genpsk | sudo tee claves/marta.psk >/dev/null

$ sudo chmod 0600 claves/*.key claves/*.psk
$ sudo chmod 0644 claves/*.pub
$ sudo ls -l claves/
-rw------- 1 root root 45 ago 18 10:12 marta.psk
-rw------- 1 root root 45 ago 18 10:11 servidor.key
-rw-r--r-- 1 root root 45 ago 18 10:11 servidor.pub

Los tres comandos merecen una nota: wg genkey genera una clave privada Curve25519 en base64; wg pubkey deriva la pública leyendo la privada de la entrada estándar; wg genpsk genera 32 bytes aleatorios para la clave precompartida. La derivación es de un solo sentido: de la pública no se puede volver a la privada.

La clave privada del servidor nunca sale del servidor. La clave privada de cada cliente nunca sale del cliente. Eso significa que, idealmente, es el cliente quien genera su par y te envía solo la pública. En la práctica, para un móvil, suele generarse en el servidor y transferirse por QR; es un compromiso aceptable si la transferencia es directa y el fichero se borra después.

El plan de direccionamiento

Antes de escribir nada, hay que decidir el rango de la VPN. Tiene que no solaparse con ninguna red que los clientes puedan usar:

Red Rango Comentario
Interna de Tramontana 10.0.2.0/24 La que hay que alcanzar
VPN 10.8.0.0/24 Elegida: poco habitual en routers domésticos
Casa de Marta 192.168.1.0/24 Muy habitual: evitar ese rango
Hoteles y cafeterías 192.168.0.0/24, 10.0.0.0/24 Evitar también

Ese 10.0.0.0/24 de la última fila es una trampa real: es el rango por defecto de muchos routers, y si un cliente está en una red 10.0.2.0/24 —la misma que la interna de Tramontana— tendrá un conflicto de rutas irresoluble. Elegir rangos poco frecuentes para la infraestructura propia evita ese problema para siempre.

Par IP en la VPN Dispositivo
servidor 10.8.0.1 srv-tramontana
operador-portatil 10.8.0.2 Tu portátil
marta-portatil 10.8.0.3 Portátil de Marta
marta-movil 10.8.0.4 Móvil de Marta
luis-portatil 10.8.0.5 Portátil de Luis

Configuración del servidor

# /etc/wireguard/wg0.conf
# Permisos 0600, propiedad de root: contiene la clave privada.

[Interface]
# Direccion de ESTE par dentro de la VPN. La mascara /24 hace que el
# kernel anada automaticamente una ruta a toda la red 10.8.0.0/24
# por wg0 al levantar la interfaz.
Address = 10.8.0.1/24

# Puerto UDP de escucha. Elegir uno alto y poco habitual reduce el
# ruido de los escaneres, aunque NO es una medida de seguridad real:
# WireGuard no responde a quien no tiene clave, asi que el puerto es
# indistinguible de uno cerrado en cualquier caso.
ListenPort = 51820

# La clave privada. Se inyecta desde fichero para no tenerla en claro
# aqui: PostUp la lee. Alternativa mas simple pero menos limpia:
#   PrivateKey = <contenido de servidor.key>
PostUp = wg set %i private-key /etc/wireguard/claves/servidor.key

# --- Reglas de red al levantar la interfaz ---
# %i se sustituye por el nombre de la interfaz (wg0).
#
# 1. Permitir el reenvio de paquetes que entran o salen por wg0
# 2. Enmascarar el trafico que sale hacia la red interna, para que las
#    respuestas vuelvan al servidor VPN y no se pierdan buscando 10.8.0.x
PostUp = nft add table inet wg
PostUp = nft 'add chain inet wg forward { type filter hook forward priority 0; }'
PostUp = nft add rule inet wg forward iifname "%i" oifname "enp0s3" accept
PostUp = nft add rule inet wg forward iifname "enp0s3" oifname "%i" ct state related,established accept
PostUp = nft 'add chain inet wg postrouting { type nat hook postrouting priority 100; }'
PostUp = nft add rule inet wg postrouting oifname "enp0s3" ip saddr 10.8.0.0/24 masquerade

# Al bajar la interfaz se elimina la tabla entera: una sola orden,
# idempotente, sin dejar reglas huerfanas acumuladas.
PostDown = nft delete table inet wg

# Contabilidad: quien se conecta y desde donde queda en el journal
PostUp = logger -t wireguard "interfaz %i levantada"
PostDown = logger -t wireguard "interfaz %i bajada"

# ==========================================================
#                        P A R E S
# ==========================================================

# --- operador (portatil de administracion) ---
[Peer]
# PublicKey identifica al par. Es su nombre criptografico.
PublicKey = aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5aB6c=
# Clave precompartida: capa adicional simetrica (apartado 14)
PresharedKey = zY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA7zY6x=
# AllowedIPs para un CLIENTE: solo su propia IP, con mascara /32.
# Significa dos cosas: (1) el trafico hacia 10.8.0.2 va a este par;
# (2) este par SOLO puede enviar paquetes con origen 10.8.0.2.
AllowedIPs = 10.8.0.2/32

# --- Marta, portatil ---
[Peer]
PublicKey = cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e=
PresharedKey = xW7vU6tS5rQ4pO3nM2lK1jI0hG9fE8dC7bA6zY5xW4v=
AllowedIPs = 10.8.0.3/32

# --- Marta, movil ---
[Peer]
PublicKey = eF7gH8iJ9kL0mN1oP2qR3sT4uV5wX6yZ7aB8cD9eF0g=
PresharedKey = vU5tS4rQ3pO2nM1lK0jI9hG8fE7dC6bA5zY4xW3vU2t=
AllowedIPs = 10.8.0.4/32
# El movil esta detras de NAT y cambia de red constantemente. El
# keepalive lo pone el CLIENTE, no el servidor: es quien tiene que
# mantener viva la asociacion NAT de su operadora.

# --- Luis, portatil (acceso restringido) ---
[Peer]
PublicKey = gH9iJ0kL1mN2oP3qR4sT5uV6wX7yZ8aB9cD0eF1gH2i=
PresharedKey = tS3rQ2pO1nM0lK9jI8hG7fE6dC5bA4zY3xW2vU1tS0r=
AllowedIPs = 10.8.0.5/32
$ sudo chmod 0600 /etc/wireguard/wg0.conf
$ sudo chown root:root /etc/wireguard/wg0.conf

Cuatro detalles de esa configuración que conviene entender:

Address = 10.8.0.1/24, con /24 y no /32. La máscara determina la ruta que wg-quick añade automáticamente: con /24, todo 10.8.0.0/24 se enruta por wg0. Con /32 habría que añadir las rutas a mano.

Las reglas van en una tabla inet wg propia. Es la aplicación de lo aprendido en 06-03: una tabla separada se elimina de un golpe con nft delete table y no interfiere con las reglas que gestiona ufw. La alternativa —añadir reglas a la tabla existente— deja restos acumulados cada vez que se reinicia la interfaz.

La clave privada se lee de un fichero con PostUp. Ponerla directamente en wg0.conf funciona igual, pero mezcla secreto y configuración en un solo fichero, lo que complica gestionarlo con Ansible sin no_log en todo. Separarlos permite versionar la configuración y tratar la clave aparte.

AllowedIPs = 10.8.0.2/32 para cada cliente, con /32. Es lo correcto y es donde más gente se equivoca. Ver el apartado 9.

Configuración de los clientes: portátil y móvil

Portátil Linux

# /etc/wireguard/wg0.conf en el portatil de operador
[Interface]
Address = 10.8.0.2/32
PrivateKey = <clave privada del portatil, generada AQUI>
# DNS interno mientras el tunel esta activo (apartado 10)
DNS = 10.0.2.15

[Peer]
# La clave PUBLICA del servidor
PublicKey = kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=
PresharedKey = zY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA7zY6x=

# Donde esta el servidor. Se puede usar un nombre DNS: WireGuard lo
# resuelve al levantar la interfaz, lo que permite IP dinamica.
Endpoint = vpn.tramontana.example:51820

# --- TUNEL DIVIDIDO: solo la red interna y la VPN van por el tunel ---
# El resto del trafico del portatil (navegacion, correo, videollamadas)
# sale por su conexion normal.
AllowedIPs = 10.0.2.0/24, 10.8.0.0/24

# Mantener viva la asociacion de NAT del router del cliente. 25 s es
# el valor recomendado: por debajo del tiempo de expiracion tipico
# de las tablas NAT (30 s en muchos routers domesticos).
PersistentKeepalive = 25
$ sudo systemctl enable --now wg-quick@wg0
$ sudo wg show
interface: wg0
  public key: aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5aB6c=
  private key: (hidden)
  listening port: 45182

peer: kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=
  preshared key: (hidden)
  endpoint: 203.0.113.45:51820
  allowed ips: 10.0.2.0/24, 10.8.0.0/24
  latest handshake: 8 seconds ago
  transfer: 12.41 KiB received, 18.02 KiB sent
  persistent keepalive: every 25 seconds

Móvil, con código QR

Las aplicaciones oficiales de WireGuard para Android e iOS leen la configuración desde un QR, lo que evita teclear cuarenta y cuatro caracteres en base64 tres veces:

$ sudo qrencode -t ansiutf8 < /etc/wireguard/clientes/marta-movil.conf
█████████████████████████████████████
██ ▄▄▄▄▄ █▀ █▀▀██▀▄ ▀█ ▄█ ▄▄▄▄▄ ██
██ █   █ █▄  ▀▄█▄▀▄█▄ ▀█ █   █ ██
██ █▄▄▄█ █ ▀▄ ▄▀▄ █▀▄▀██ █▄▄▄█ ██
...

# Y para enviarlo por un canal seguro, como PNG
$ sudo qrencode -t png -o /tmp/marta-movil.png \
      < /etc/wireguard/clientes/marta-movil.conf

Advertencia importante sobre el QR: contiene la clave privada en claro. No se envía por correo, ni por WhatsApp, ni se deja en /tmp. Se muestra en el terminal, se escanea delante del móvil, y se borra:

$ shred -u /tmp/marta-movil.png

Un QR de WireGuard fotografiado por encima del hombro es acceso completo a la red interna.

# marta-movil.conf
[Interface]
Address = 10.8.0.4/32
PrivateKey = <generada para este movil>
DNS = 10.0.2.15

[Peer]
PublicKey = kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=
PresharedKey = vU5tS4rQ3pO2nM1lK0jI9hG8fE7dC6bA5zY4xW3vU2t=
Endpoint = vpn.tramontana.example:51820
AllowedIPs = 10.0.2.0/24, 10.8.0.0/24
# En movil, el keepalive es imprescindible: sin el, la asociacion NAT
# de la operadora caduca y el servidor no puede iniciar trafico hacia
# el telefono hasta que este envie algo.
PersistentKeepalive = 25

En la aplicación móvil conviene activar además «VPN a demanda» excluyendo la red Wi-Fi de la oficina: así el túnel se levanta automáticamente fuera y no molesta dentro.

Enrutamiento, NAT y túnel dividido frente a túnel completo

El reenvío de paquetes

Por defecto, Linux no reenvía paquetes entre interfaces: es una máquina, no un router. Para que el tráfico que llega por wg0 salga hacia 10.0.2.0/24 hay que activarlo:

$ cat /proc/sys/net/ipv4/ip_forward
0

# Persistente, en el fichero de endurecimiento que ya existe (06-06)
$ echo 'net.ipv4.ip_forward = 1' | \
      sudo tee /etc/sysctl.d/72-wireguard.conf
$ echo 'net.ipv6.conf.all.forwarding = 0' | \
      sudo tee -a /etc/sysctl.d/72-wireguard.conf
$ sudo sysctl --system | grep forward
net.ipv4.ip_forward = 1

El IPv6 se deja desactivado deliberadamente: si no lo usas, activarlo abre un camino que no estás vigilando. Es la misma lógica de 06-06.

Por qué hace falta enmascaramiento

Marta, desde 10.8.0.3, consulta PostgreSQL en 10.0.2.15. Sin NAT, el paquete llega a PostgreSQL con origen 10.8.0.3, PostgreSQL responde a 10.8.0.3... y su tabla de rutas no sabe dónde está esa red, así que envía la respuesta a la puerta de enlace por defecto y se pierde.

Hay dos soluciones:

Enmascaramiento (NAT) Ruta estática en cada destino
Qué hace El servidor VPN reescribe el origen Cada máquina interna aprende a llegar a 10.8.0.0/24
Configuración Una regla, en un sitio Una ruta en cada máquina o en el router
Los servicios internos ven La IP del servidor VPN La IP real del cliente VPN
Registros y control por IP Se pierde la granularidad Se conserva
Cuándo usarlo Redes que no controlas Redes propias, y es preferible

En Tramontana, con todo dentro de 10.0.2.0/24 y el servidor VPN siendo la misma máquina, el enmascaramiento simplifica y el coste es aceptable. En una red más grande, la ruta estática en el router es mejor: permite que pg_hba.conf distinga a Marta de Luis por su IP de VPN, y que los registros de acceso sean útiles.

Túnel dividido frente a túnel completo

Es la decisión de diseño con más consecuencias de toda la lección, y se toma con una sola línea: AllowedIPs en el cliente.

Túnel dividido Túnel completo
AllowedIPs del cliente 10.0.2.0/24, 10.8.0.0/24 0.0.0.0/0, ::/0
Qué va por el túnel Solo lo interno Todo
Ancho de banda del servidor Mínimo Todo el tráfico de todos
Latencia de la navegación normal Sin cambios Peor: rodea por el servidor
Videollamadas y streaming Directos Por el servidor: pueden ir mal
Protege en Wi-Fi público Solo el tráfico interno Todo el tráfico
Privacidad del usuario Se respeta La empresa ve toda su navegación
Permite forzar políticas de salida No Sí: filtrado, registro
Fuga de DNS posible Sí, hay que cuidarlo No

Para Tramontana la decisión es el túnel dividido, y con tres argumentos:

  1. Ancho de banda. Todo el tráfico de tres personas pasando por un servidor de 2 vCPU con una conexión compartida degradaría la navegación de todos y competiría con la aplicación de reservas, que es lo que da de comer.
  2. Privacidad. Con túnel completo, toda la navegación personal de Marta —incluida la que hace fuera de horario en su portátil de empresa— pasa por un servidor de la empresa y aparece en sus registros. Eso tiene implicaciones de protección de datos y, sobre todo, no es necesario para el objetivo.
  3. El objetivo es acceder a lo interno, no proteger la navegación general. Para eso están HTTPS y el sentido común en redes públicas.

Cuándo sí el túnel completo: un portátil corporativo que debe cumplir la política de filtrado de la empresa esté donde esté, o alguien que trabaja habitualmente desde redes que no son de fiar y necesita que todo vaya cifrado. En ese caso, AllowedIPs = 0.0.0.0/0 en el cliente, y las reglas de enmascaramiento del servidor ya lo soportan tal como están escritas.

DNS dentro del túnel

Con túnel dividido aparece un detalle fácil de pasar por alto: los nombres internos.

# Sin DNS interno, con el tunel levantado
$ dig +short srv-tramontana.interno
# (vacio: el DNS del hotel no conoce ese nombre)

La directiva DNS = 10.0.2.15 del cliente hace que wg-quick configure el resolvedor mientras el túnel está activo. En Ubuntu con systemd-resolved, wg-quick usa resolvectl y hace algo elegante:

$ resolvectl status wg0
Link 5 (wg0)
    Current Scopes: DNS
         Protocols: +DefaultRoute
Current DNS Server: 10.0.2.15
       DNS Servers: 10.0.2.15

Con ~tramontana.example como dominio de búsqueda, solo las consultas de ese dominio irían al DNS interno y el resto seguiría por el DNS normal — lo que se llama DNS dividido y es lo ideal con túnel dividido:

# En el cliente, para DNS dividido de verdad
PostUp = resolvectl dns %i 10.0.2.15; resolvectl domain %i '~tramontana.example'

La fuga de DNS es el problema simétrico: si el cliente sigue usando el DNS del hotel para todo, ese servidor ve qué nombres internos consultas, lo que revela información sobre tu infraestructura. Comprobarlo:

$ resolvectl query panel.tramontana.example
panel.tramontana.example: 10.0.2.20 -- link: wg0

$ sudo tcpdump -ni enp0s3 port 53 -c 5
# Con DNS dividido bien configurado, aqui NO deben aparecer
# consultas de nombres internos.

Requisito previo, claro: que haya un DNS interno que resuelva esos nombres. Con tres máquinas, dnsmasq en srv-tramontana basta y sobra.

Integración con el cortafuegos y cierre de puertos

Este es el apartado donde la VPN paga su coste, cerrando puertos que llevaban abiertos desde el Módulo 6.

# 1. Abrir el puerto de WireGuard. UDP, y solo ese.
$ sudo ufw allow 51820/udp comment 'WireGuard'

# 2. Permitir el trafico que llega POR el tunel
$ sudo ufw allow in on wg0 comment 'Trafico interno de la VPN'

# 3. Permitir el reenvio, que por defecto ufw deniega
$ sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' \
      /etc/default/ufw
$ sudo ufw reload

Y ahora la parte importante. Antes de cerrar nada, hay que verificar que la VPN funciona — es literalmente el caso de «nunca cierres la puerta por la que estás entrando»:

# --- VERIFICAR PRIMERO, desde el cliente con el tunel levantado ---
$ psql -h 10.0.2.15 -U operador -d tramontana -c 'SELECT 1;' >/dev/null && echo OK
OK
$ curl -sI http://10.0.2.20:8404/ | head -1
HTTP/1.1 200 OK
$ ssh [email protected] 'hostname'
srv-tramontana

# --- SOLO ENTONCES, cerrar ---
$ sudo ufw status numbered | grep -E '5432|8404|22'
[ 3] 5432/tcp    ALLOW IN    10.0.2.0/24
[ 5] 8404/tcp    ALLOW IN    10.0.2.0/24
[ 7] 22/tcp      LIMIT IN    Anywhere

# PostgreSQL: solo desde la VPN y desde la propia red interna
$ sudo ufw delete 5
$ sudo ufw delete 3
$ sudo ufw allow from 10.8.0.0/24 to any port 5432 proto tcp comment 'PostgreSQL via VPN'
$ sudo ufw allow from 10.8.0.0/24 to any port 8404 proto tcp comment 'Panel via VPN'

# SSH: se mantiene abierto con LIMIT como red de seguridad. Cerrarlo
# del todo significa que un fallo de WireGuard te deja sin acceso a
# la maquina. Se cierra cuando haya una segunda via probada (consola
# del proveedor o KVM), no antes.

Esa última decisión merece defenderse, porque la tentación de cerrar el 22 es fuerte. No se cierra SSH hasta tener una segunda vía de acceso probada. Si WireGuard falla tras una actualización del kernel, o si la configuración se corrompe, o si el módulo no carga, el 22 abierto con LIMIT y fail2ban es la diferencia entre un susto y un desplazamiento físico al centro de datos. Es exactamente la lección de 06-02.

Resultado final:

$ sudo ufw status verbose
Estado: activo
Politica por defecto: deny (incoming), allow (outgoing), allow (routed)

Hasta                       Acción      Desde
-----                       ------      -----
22/tcp                      LIMIT IN    Anywhere
80,443/tcp                  ALLOW IN    Anywhere
51820/udp (WireGuard)       ALLOW IN    Anywhere
Anywhere on wg0             ALLOW IN    Anywhere
5432/tcp (PostgreSQL VPN)   ALLOW IN    10.8.0.0/24
8404/tcp (Panel VPN)        ALLOW IN    10.8.0.0/24

Qué ha cambiado, y es el resumen del proyecto:

Servicio Antes Después
PostgreSQL 5432 Abierto a 10.0.2.0/24; inalcanzable desde fuera Solo por VPN, alcanzable desde cualquier sitio
Panel 8404 Igual Solo por VPN
Puertos TCP expuestos a Internet 22, 80, 443 22, 80, 443 (sin cambios)
Puertos UDP expuestos 0 1, que no responde a quien no tiene clave
Acceso remoto de Marta SSH y trabajar en el servidor Directo, con sus herramientas

Verificación

# 1. La interfaz existe y tiene su direccion
$ ip -brief addr show wg0
wg0   UNKNOWN   10.8.0.1/24

# 2. Las rutas que ha creado wg-quick
$ ip route show dev wg0
10.8.0.0/24 proto kernel scope link src 10.8.0.1

# 3. Estado de los pares (desde el servidor)
$ sudo wg show
interface: wg0
  public key: kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=
  private key: (hidden)
  listening port: 51820

peer: cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e=
  preshared key: (hidden)
  endpoint: 198.51.100.87:41922
  allowed ips: 10.8.0.3/32
  latest handshake: 41 seconds ago
  transfer: 4.18 MiB received, 28.44 MiB sent

peer: eF7gH8iJ9kL0mN1oP2qR3sT4uV5wX6yZ7aB8cD9eF0g=
  allowed ips: 10.8.0.4/32
  # sin 'latest handshake': este par NUNCA se ha conectado

Cómo se lee wg show, que es la herramienta principal de diagnóstico:

Campo Qué significa
endpoint Dónde está el par ahora. Ausente = nunca se ha visto
latest handshake Cuándo. Más de 3 min = inactivo o desconectado
transfer Bytes cifrados intercambiados
Sin latest handshake El par nunca ha conectado: revisar claves o red
# 4. Conectividad basica (desde el cliente)
$ ping -c3 10.8.0.1
64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=24.1 ms

# 5. Alcanzar la red interna
$ ping -c2 10.0.2.15 && nc -z -v 10.0.2.15 5432
Connection to 10.0.2.15 5432 port [tcp/postgresql] succeeded!

# 6. COMPROBAR QUE EL TRAFICO VA CIFRADO
# En enp0s3 (la interfaz fisica) solo se ve UDP opaco:
$ sudo tcpdump -ni enp0s3 udp port 51820 -c 3 -X | head -12
14:02:11.482 IP 198.51.100.87.41922 > 10.0.2.15.51820: UDP, length 128
	0x0000:  4500 009c 0000 4000 3611 ...
	0x0020:  0400 0000 9f2c 1a4b 8e73 d012 4a91 ...
# No hay ni un byte legible: ni "SELECT", ni cabeceras, nada.

# En wg0 (dentro del tunel) se ve el trafico REAL, ya descifrado:
$ sudo tcpdump -ni wg0 -c 3
14:02:14.118 IP 10.8.0.3.51422 > 10.0.2.15.5432: Flags [P.], length 34
14:02:14.121 IP 10.0.2.15.5432 > 10.8.0.3.51422: Flags [P.], length 8

# 7. Con tunel dividido, la IP de salida NO cambia
$ curl -s https://ifconfig.me; echo
198.51.100.87        # <- la del hotel, no la del servidor

# Con tunel completo (AllowedIPs = 0.0.0.0/0) seria:
# 203.0.113.45       # <- la del servidor VPN

# 8. Comprobar que no hay fuga de DNS
$ resolvectl query srv-tramontana.interno
srv-tramontana.interno: 10.0.2.15 -- link: wg0

La comprobación 6 es la que demuestra el valor de todo el montaje: dos capturas de la misma comunicación, una ilegible en la red pública y otra legible dentro del túnel. Es lo que hay que enseñar cuando alguien pregunta qué hace exactamente una VPN.

Y la 7 es la que confirma la decisión de túnel dividido: la navegación normal de Marta sigue saliendo por su conexión, como debe ser.

Gestión de clientes: alta, baja y rotación

Alta de un cliente

#!/usr/bin/env bash
#
# alta_vpn.sh - Da de alta un cliente de WireGuard
#
# Uso: alta_vpn.sh <nombre> [ip]
#
set -euo pipefail

readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comunes.sh"

readonly WG_DIR=/etc/wireguard
readonly WG_IF=wg0
readonly WG_RED=10.8.0
readonly WG_ENDPOINT="vpn.tramontana.example:51820"
readonly WG_ALLOWED="10.0.2.0/24, 10.8.0.0/24"

umask 077

siguiente_ip() {
    # La primera libre a partir de .2, mirando la configuracion real
    local usadas i
    usadas="$(grep -oP 'AllowedIPs\s*=\s*'"${WG_RED}"'\.\K[0-9]+' \
        "${WG_DIR}/${WG_IF}.conf" | sort -n)"
    for i in $(seq 2 254); do
        grep -qx "$i" <<<"$usadas" || { echo "${WG_RED}.${i}"; return 0; }
    done
    morir 73 "no quedan direcciones libres en ${WG_RED}.0/24"
}

main() {
    local nombre="${1:?uso: alta_vpn.sh <nombre> [ip]}"
    [[ "$nombre" =~ ^[a-z0-9-]+$ ]] || morir 2 "nombre invalido: solo [a-z0-9-]"
    [[ -f "${WG_DIR}/clientes/${nombre}.conf" ]] && morir 2 "el cliente $nombre ya existe"

    requiere_comando wg
    requiere_comando qrencode

    local ip; ip="${2:-$(siguiente_ip)}"
    log "dando de alta '$nombre' con IP $ip"

    mkdir -p "${WG_DIR}/clientes" "${WG_DIR}/claves"

    local priv pub psk
    priv="$(wg genkey)"
    pub="$(wg pubkey <<<"$priv")"
    psk="$(wg genpsk)"

    # 1. Configuracion del cliente
    cat > "${WG_DIR}/clientes/${nombre}.conf" <<EOF
[Interface]
Address = ${ip}/32
PrivateKey = ${priv}
DNS = 10.0.2.15

[Peer]
PublicKey = $(wg pubkey < "${WG_DIR}/claves/servidor.key")
PresharedKey = ${psk}
Endpoint = ${WG_ENDPOINT}
AllowedIPs = ${WG_ALLOWED}
PersistentKeepalive = 25
EOF
    chmod 0600 "${WG_DIR}/clientes/${nombre}.conf"

    # 2. Anadir el par a la configuracion persistente
    cat >> "${WG_DIR}/${WG_IF}.conf" <<EOF

# --- ${nombre} (alta: $(date +%F)) ---
[Peer]
PublicKey = ${pub}
PresharedKey = ${psk}
AllowedIPs = ${ip}/32
EOF

    # 3. Y en CALIENTE, sin cortar a los demas. 'wg syncconf' aplica los
    #    cambios sin bajar la interfaz; 'wg-quick down/up' cortaria a
    #    todos los clientes conectados.
    wg syncconf "$WG_IF" <(wg-quick strip "$WG_IF")

    log "cliente '$nombre' dado de alta"
    printf '\nEscanea este codigo con la app de WireGuard:\n\n'
    qrencode -t ansiutf8 < "${WG_DIR}/clientes/${nombre}.conf"
    printf '\nConfiguracion en: %s/clientes/%s.conf\n' "$WG_DIR" "$nombre"
    printf 'BORRALA del servidor tras entregarla al usuario.\n'
}

main "$@"
$ sudo ~/scripts/alta_vpn.sh marta-tableta
[2026-08-18 14:22:01] dando de alta 'marta-tableta' con IP 10.8.0.6
[2026-08-18 14:22:01] cliente 'marta-tableta' dado de alta

Escanea este codigo con la app de WireGuard:
█████████████████████████
...

La línea clave es wg syncconf: aplica los cambios sin bajar la interfaz, de modo que dar de alta a un cliente nuevo no corta a los que están conectados. Con wg-quick down && wg-quick up se cortarían todos, y es un error frecuente.

Baja de un cliente

# Inmediata, en caliente: basta con quitar la clave publica
$ sudo wg set wg0 peer 'cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e=' remove

# Y de la configuracion persistente, para que no vuelva al reiniciar
$ sudo sed -i '/--- marta-portatil/,/^$/d' /etc/wireguard/wg0.conf
$ sudo wg-quick strip wg0 | sudo wg syncconf wg0 /dev/stdin
$ sudo shred -u /etc/wireguard/clientes/marta-portatil.conf

Por qué basta con quitar el par, y es una ventaja arquitectónica de WireGuard: no hay lista de revocación, ni certificados que sigan siendo válidos, ni ventana de caducidad. El servidor solo acepta paquetes de claves públicas que estén en su configuración. En cuanto la clave desaparece, ese dispositivo es indistinguible de cualquier desconocido de Internet: sus paquetes se descartan sin respuesta.

Compáralo con OpenVPN, donde revocar un certificado exige actualizar la CRL, asegurarse de que el servidor la lee, y que no haya sesiones activas. Aquí es una orden que surte efecto en el siguiente paquete.

Rotación de claves

Situación Acción Urgencia
Robo o pérdida de un dispositivo Quitar el par inmediatamente Minutos
Alguien deja la empresa Quitar el par El mismo día
Rotación preventiva Regenerar el par del cliente Anual
Compromiso del servidor Regenerar TODO: servidor y todos los clientes Inmediata

La rotación de la clave del servidor es la operación disruptiva, porque su clave pública está en la configuración de todos los clientes. El procedimiento sin cortes: levantar una segunda interfaz wg1 en otro puerto con la clave nueva, migrar los clientes uno a uno, y retirar wg0 cuando el último haya migrado.

Seguridad: lo que protege y lo que no

La clave privada nunca sale del dispositivo

Es el principio básico, y tiene una implicación operativa concreta: lo correcto es que el cliente genere su propio par y te envíe únicamente la clave pública. La clave pública se puede enviar por correo sin ningún problema.

# En el portatil de Luis
$ umask 077 && wg genkey | tee privada.key | wg pubkey
gH9iJ0kL1mN2oP3qR4sT5uV6wX7yZ8aB9cD0eF1gH2i=
# Luis envia SOLO esa linea. La privada no sale de su portatil.

El script alta_vpn.sh genera el par en el servidor porque para un móvil es lo práctico. Es un compromiso consciente, y por eso el script insiste en borrar el fichero tras entregarlo.

La clave precompartida

PresharedKey = zY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA7zY6x=

Es una capa de cifrado simétrico de 256 bits que se mezcla en la derivación de claves, además del intercambio Curve25519.

Contra qué protege, que es una amenaza futura y no actual: un ordenador cuántico suficientemente grande podría romper Curve25519 mediante el algoritmo de Shor. No existe hoy, ni está cerca, pero existe un modelo de amenaza llamado «cosechar ahora, descifrar después»: un adversario con recursos captura y almacena tráfico cifrado hoy para descifrarlo dentro de quince años. La clave precompartida, al ser simétrica, es resistente a ese ataque —Grover solo reduce la seguridad efectiva de 256 a 128 bits, lo que sigue siendo inalcanzable.

Contra qué no protege: nada de lo que ocurra hoy. No añade seguridad frente a atacantes clásicos. Es una póliza barata contra un riesgo lejano, y por eso está en la configuración: cuesta una línea.

Lo que hay que decir en voz alta

Una VPN no convierte una red interna en una red segura.

Es la conclusión más importante de la lección, y la más ignorada. Con la VPN montada, cualquiera que llegue por el túnel está dentro de 10.0.2.0/24 y ve todo lo que ve una máquina de esa red. Si el portátil de Luis se infecta con un troyano, ese troyano tiene acceso directo a PostgreSQL, al panel de estadísticas y a cualquier servicio que confíe en la red interna.

Amenaza ¿La VPN protege?
Espionaje del tráfico en una red pública Sí
Escaneo de puertos desde Internet Sí: no hay puertos TCP nuevos
Fuerza bruta contra PostgreSQL desde Internet Sí: ya no es alcanzable
Portátil de un usuario infectado No: el troyano entra por el túnel
Contraseña débil en un servicio interno No
Vulnerabilidad de un servicio interno No, si el atacante llega por VPN
Un empleado descontento No
Movimiento lateral una vez dentro No

Lo que se sigue de esa tabla es la arquitectura de confianza cero: cada servicio autentica y cifra por su cuenta, sin confiar en la red. En la práctica, para Tramontana, significa que la VPN no sustituye a nada de lo que ya tienes: pg_hba.conf sigue exigiendo scram-sha-256, el panel de estadísticas sigue necesitando su restricción, SSH sigue con claves y fail2ban, y TLS sigue haciendo falta dentro. La VPN es una capa más, no un perímetro que permita relajar las demás.

Y una restricción adicional que la VPN sí permite hacer bien, aprovechando AllowedIPs como control de acceso:

# Luis es desarrollador: no necesita PostgreSQL de produccion.
# Su IP de VPN es fija (10.8.0.5), asi que se puede filtrar.
$ sudo ufw deny from 10.8.0.5 to any port 5432 proto tcp \
      comment 'Luis: sin acceso a la BD de produccion'
$ sudo ufw allow from 10.8.0.5 to any port 8404 proto tcp

Como cada par tiene una IP fija garantizada por el propio protocolo —recuerda: AllowedIPs impide suplantarla—, esta regla es fiable de una forma que no lo sería en una red normal.

Automatización con Ansible

# ~/tramontana-infra/roles/vpn/defaults/main.yml
---
vpn_interfaz: wg0
vpn_puerto: 51820
vpn_red: 10.8.0.0/24
vpn_ip_servidor: 10.8.0.1
vpn_interfaz_salida: enp0s3
vpn_endpoint: vpn.tramontana.example
vpn_dns_interno: 10.0.2.15
vpn_redes_accesibles: "10.0.2.0/24, 10.8.0.0/24"
vpn_clientes:
  - { nombre: operador-portatil, ip: 10.8.0.2, acceso_bd: true }
  - { nombre: marta-portatil,    ip: 10.8.0.3, acceso_bd: true }
  - { nombre: marta-movil,       ip: 10.8.0.4, acceso_bd: false }
  - { nombre: luis-portatil,     ip: 10.8.0.5, acceso_bd: false }

Las claves públicas y precompartidas viven en Vault, integrado con pass según 07-06:

# group_vars/all/vault.yml (cifrado con ansible-vault)
vault_vpn_clientes:
  operador-portatil:
    pubkey: "aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5aB6c="
    psk: "zY9xW8vU7tS6rQ5pO4nM3lK2jI1hG0fE9dC8bA7zY6x="
  marta-portatil:
    pubkey: "cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e="
    psk: "xW7vU6tS5rQ4pO3nM2lK1jI0hG9fE8dC7bA6zY5xW4v="
# ~/tramontana-infra/roles/vpn/tasks/main.yml
---
- name: Instalar WireGuard
  ansible.builtin.apt:
    name: [wireguard, wireguard-tools, qrencode]
    state: present
  tags: [paquetes]

- name: Comprobar que el modulo del kernel esta disponible
  ansible.builtin.command: modinfo wireguard
  register: modwg
  changed_when: false
  failed_when: modwg.rc != 0

- name: Directorios de WireGuard
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: root
    mode: '0700'
  loop:
    - /etc/wireguard
    - /etc/wireguard/claves

- name: Generar la clave privada del servidor si no existe
  ansible.builtin.shell:
    cmd: "umask 077 && wg genkey > /etc/wireguard/claves/servidor.key"
    creates: /etc/wireguard/claves/servidor.key
  # 'creates' hace la tarea idempotente: NO regenera la clave en cada
  # ejecucion, lo que dejaria fuera a todos los clientes (07-06).

- name: Leer la clave publica del servidor
  ansible.builtin.shell:
    cmd: "wg pubkey < /etc/wireguard/claves/servidor.key"
  register: wg_pub
  changed_when: false

- name: Activar el reenvio de IPv4
  ansible.posix.sysctl:
    name: net.ipv4.ip_forward
    value: '1'
    sysctl_file: /etc/sysctl.d/72-wireguard.conf
    reload: true

- name: Desplegar la configuracion del servidor
  ansible.builtin.template:
    src: wg0.conf.j2
    dest: "/etc/wireguard/{{ vpn_interfaz }}.conf"
    owner: root
    group: root
    mode: '0600'
    backup: true
  no_log: true                    # contiene claves precompartidas
  notify: Sincronizar wireguard

- name: Generar la configuracion de cada cliente
  ansible.builtin.template:
    src: cliente.conf.j2
    dest: "/etc/wireguard/clientes/{{ item.nombre }}.conf"
    owner: root
    group: root
    mode: '0600'
  loop: "{{ vpn_clientes }}"
  loop_control:
    label: "{{ item.nombre }}"
  no_log: true
  tags: [clientes]

- name: Abrir el puerto de WireGuard
  community.general.ufw:
    rule: allow
    port: "{{ vpn_puerto }}"
    proto: udp
    comment: WireGuard
  tags: [firewall]

- name: Permitir el trafico entrante por la interfaz del tunel
  community.general.ufw:
    rule: allow
    interface: "{{ vpn_interfaz }}"
    direction: in
  tags: [firewall]

- name: Acceso a PostgreSQL solo para los clientes autorizados
  community.general.ufw:
    rule: "{{ 'allow' if item.acceso_bd else 'deny' }}"
    src: "{{ item.ip }}"
    port: '5432'
    proto: tcp
    comment: "VPN {{ item.nombre }}"
  loop: "{{ vpn_clientes }}"
  loop_control:
    label: "{{ item.nombre }}"
  tags: [firewall]

- name: Activar la interfaz
  ansible.builtin.systemd:
    name: "wg-quick@{{ vpn_interfaz }}"
    enabled: true
    state: started

- name: Forzar los handlers antes de verificar
  ansible.builtin.meta: flush_handlers

# --- Verificacion ---
- name: Comprobar que la interfaz esta levantada con su direccion
  ansible.builtin.command: "ip -brief addr show {{ vpn_interfaz }}"
  register: ipwg
  changed_when: false
  failed_when: vpn_ip_servidor not in ipwg.stdout
  tags: [verificar]

- name: Comprobar que todos los pares configurados estan presentes
  ansible.builtin.command: "wg show {{ vpn_interfaz }} peers"
  register: pares
  changed_when: false
  failed_when: pares.stdout_lines | length != vpn_clientes | length
  tags: [verificar]
# roles/vpn/handlers/main.yml
---
- name: Sincronizar wireguard
  # 'wg syncconf' aplica los cambios SIN BAJAR la interfaz: no corta a
  # los clientes conectados. 'wg-quick down/up' los cortaria a todos,
  # y a ti mismo si estas administrando POR la VPN.
  ansible.builtin.shell:
    cmd: "wg syncconf {{ vpn_interfaz }} <(wg-quick strip {{ vpn_interfaz }})"
    executable: /bin/bash
  changed_when: true

Dos detalles de este rol que resumen lo aprendido en 07-06: el creates: que impide regenerar la clave del servidor en cada ejecución —lo que dejaría fuera a todos los clientes de golpe—, y el handler con wg syncconf en lugar de reiniciar, que es la versión WireGuard del reload en vez de restart de 08-01.

Operación y problemas habituales

Qué mirar

# Pares y ultimo saludo, en una linea por par
$ sudo wg show wg0 dump | awk 'NR>1 {
    printf "%-12s %-22s %s\n", substr($1,1,10)"...", $3,
    ($5==0 ? "NUNCA" : strftime("%F %T", $5)) }'
cD5eF6gH7i... 198.51.100.87:41922    2026-08-18 14:02:11
eF7gH8iJ9k... (none)                 NUNCA

# Volumen por par (util para detectar un cliente descontrolado)
$ sudo wg show wg0 transfer
cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e=	4382144	29827072
Frecuencia Qué se mira Señal de alarma
Continuo (08-06) Interfaz wg0 levantada Ausente
Continuo Pares con saludo reciente Ninguno en horario laboral
Semanal Pares que nunca han conectado Alta que no se completó, o clave mal entregada
Mensual Pares sin actividad en 90 días Candidatos a baja
Mensual Tráfico anómalo por par Un cliente comprometido

Los cinco problemas que aparecen de verdad

1. AllowedIPs solapados — el error más común y el más desconcertante.

# MAL: dos pares reclaman la misma direccion
[Peer]   # marta
AllowedIPs = 10.8.0.3/32
[Peer]   # luis
AllowedIPs = 10.8.0.0/24     # <-- incluye 10.8.0.3

WireGuard resuelve el enrutamiento por prefijo más específico, igual que la tabla de rutas. Con esa configuración, el tráfico a 10.8.0.3 va a Marta (más específico), pero Luis puede enviar paquetes falsificando cualquier IP de 10.8.0.0/24, porque su filtro de entrada se lo permite. Es una brecha de seguridad, no solo un problema de enrutamiento.

Peor aún: AllowedIPs = 0.0.0.0/0 en un par del servidor significa «todo el tráfico de Internet va por este par». Es correcto en la configuración de un cliente con túnel completo y catastrófico en la del servidor: rompe toda su conectividad.

Regla: en el servidor, cada cliente lleva solo su /32. 0.0.0.0/0 solo aparece en la configuración del cliente, nunca en la del servidor.

# Detectar solapamientos antes de que muerdan
$ sudo wg show wg0 allowed-ips | awk '{$1=""; print}' | tr ' ' '\n' | \
      grep -v '^$' | sort | uniq -d
# (vacio: sin solapamientos)

2. El saludo no ocurre nunca.

$ sudo wg show wg0 | grep -A2 'peer:'
peer: eF7gH8iJ9kL0mN1oP2qR3sT4uV5wX6yZ7aB8cD9eF0g=
  allowed ips: 10.8.0.4/32
  # sin 'latest handshake'

En orden de probabilidad: el puerto UDP no llega (cortafuegos del servidor, del router, o del proveedor del cliente); las claves no se corresponden (la pública del cliente en el servidor no deriva de su privada); el Endpoint es incorrecto o el DNS no resuelve; o hay una PresharedKey en un lado y no en el otro.

# Comprobar que los paquetes llegan siquiera
$ sudo tcpdump -ni enp0s3 udp port 51820
# Si no aparece NADA con el cliente intentando conectar, el problema
# esta antes del servidor: red, router o cortafuegos intermedio.

# Verificar que una clave publica se corresponde con una privada
$ wg pubkey < /etc/wireguard/claves/servidor.key
kLm3Xq7pR2vN8sT4uY9wA1bC5dE6fG0hI2jK3lM4nO8=

3. Saludo correcto pero no hay tráfico. El túnel está montado y no pasa nada. Casi siempre es una de tres: falta net.ipv4.ip_forward=1, falta la regla de enmascaramiento, o falta la red de destino en los AllowedIPs del cliente.

$ sysctl net.ipv4.ip_forward
$ sudo nft list table inet wg | grep masquerade
$ sudo wg show wg0 allowed-ips

4. Se corta tras unos minutos de inactividad. Es NAT: el router del cliente o la operadora móvil olvida la asociación. Solución: PersistentKeepalive = 25 en el cliente.

5. Fragmentación y MTU. El síntoma es característico y engañoso: ping funciona, SSH conecta y se cuelga al listar un directorio grande, las páginas web cargan a medias.

# La MTU por defecto de WireGuard es 1420 (1500 - 80 de cabeceras).
# Con PPPoE u otras encapsulaciones puede seguir siendo demasiado.
$ ping -M do -s 1372 -c2 10.0.2.15      # 1372 + 28 = 1400
$ ping -M do -s 1392 -c2 10.0.2.15      # 1392 + 28 = 1420
ping: local error: message too long, mtu=1420
# En el cliente, si hay fragmentacion
[Interface]
MTU = 1380

Errores Comunes y Consejos

  • Poner AllowedIPs = 0.0.0.0/0 en un par del servidor. Rompe toda su conectividad. Ahí va solo el /32 del cliente.
  • AllowedIPs solapados entre pares. No es solo un problema de rutas: permite a un cliente suplantar las IP de otro.
  • Elegir 192.168.1.0/24 o 10.0.0.0/24 para la VPN. Chocará con la red de casa de alguien o de un hotel, y el conflicto no tiene arreglo desde tu lado.
  • Enviar el QR o el fichero de configuración por correo o mensajería. Contiene la clave privada en claro: es acceso completo a la red interna.
  • Generar las claves sin umask 077. Nacen con permisos 644, aunque sea un instante.
  • Cerrar el puerto SSH el mismo día que montas la VPN. Si WireGuard falla tras actualizar el kernel, te quedas fuera. Segunda vía probada primero.
  • Olvidar net.ipv4.ip_forward=1. El túnel se monta perfectamente y no pasa un solo paquete hacia la red interna.
  • Olvidar PersistentKeepalive en móviles. El túnel «se cae» a los pocos minutos de inactividad.
  • Usar wg-quick down && up para añadir un cliente. Corta a todos los conectados, incluido tú si administras por la VPN. Usa wg syncconf.
  • Regenerar la clave del servidor en cada ejecución de Ansible. Deja fuera a todos los clientes de golpe. creates: o stat antes.
  • Creer que la VPN hace segura la red interna. Un portátil infectado entra por el túnel con todos los permisos. Cada servicio autentica por su cuenta.
  • Quitar el TLS de los servicios internos «porque ya va por la VPN». El cifrado acaba en el servidor VPN, no en el servicio.
  • Diagnosticar sin tcpdump. Ver si los paquetes UDP llegan siquiera al servidor separa en un minuto los problemas de red de los de configuración.
  • Ignorar la MTU. El síntoma —conecta y luego se cuelga con transferencias grandes— parece cualquier cosa menos fragmentación.
  • Consejo de método. Antes de cerrar un puerto, verifica desde el cliente que ya llegas por la VPN. La regla del curso no admite excepciones: nunca cierres la puerta por la que estás entrando.

Ejercicios

Ejercicio 1

Marta te llama desde un hotel: «la VPN dice que está conectada pero no puedo entrar en el panel». Diagnostica el problema de forma sistemática, desde lo más probable a lo menos, y resuélvelo.

Ejercicio 2

Diseña el procedimiento completo de respuesta ante la pérdida del móvil de Marta con la VPN configurada, en forma de runbook, e incluye lo que hay que hacer después para que el incidente no se repita igual.

Ejercicio 3

Marta pregunta si con la VPN «ya estamos seguros» y si se pueden simplificar otras medidas ahora que la red está protegida. Redacta la respuesta.

Soluciones

Solución 1

Método: de fuera hacia dentro, y de lo más probable a lo menos. Cada paso descarta una capa entera, y ese orden ahorra la mayor parte del tiempo.

# ===== PASO 1: ¿hay saludo? (separa red de todo lo demas) =====
# Desde el servidor:
$ sudo wg show wg0 | grep -A4 'cD5eF6gH7iJ8'
peer: cD5eF6gH7iJ8kL9mN0oP1qR2sT3uV4wX5yZ6aB7cD8e=
  endpoint: 198.51.100.87:41922
  latest handshake: 14 seconds ago
  transfer: 892 B received, 0 B sent

Hay saludo reciente: el túnel está criptográficamente establecido, la clave es correcta, el puerto UDP llega y el Wi-Fi del hotel deja pasar. Eso descarta de un golpe las cuatro causas más frecuentes.

Pero fíjate en 0 B sent: el servidor recibe de Marta y no le envía nada. Ese detalle apunta directamente a enrutamiento o filtrado, no a la VPN.

# ===== PASO 2: ¿llegan los paquetes al servidor por el tunel? =====
$ sudo tcpdump -ni wg0 -c 5
14:31:02 IP 10.8.0.3.52118 > 10.0.2.20.8404: Flags [S], seq 118..., length 0
14:31:03 IP 10.8.0.3.52118 > 10.0.2.20.8404: Flags [S], seq 118..., length 0
14:31:05 IP 10.8.0.3.52118 > 10.0.2.20.8404: Flags [S], seq 118..., length 0

Tres SYN sin respuesta. El paquete de Marta atraviesa el túnel correctamente y llega al servidor. El problema está después de la VPN: algo entre el servidor y el puerto 8404 no responde. La VPN queda descartada por completo.

# ===== PASO 3: ¿es el cortafuegos? =====
$ sudo ufw status numbered | grep 8404
[ 9] 8404/tcp   ALLOW IN   10.8.0.0/24

# La regla existe. ¿Se esta aplicando? Contar aciertos:
$ sudo nft list ruleset | grep -B2 -A2 8404
$ sudo journalctl -k --since "5 min ago" | grep -i 'UFW BLOCK' | tail -3
[UFW BLOCK] IN=wg0 OUT=enp0s3 SRC=10.8.0.3 DST=10.0.2.20 PROTO=TCP DPT=8404

Ahí está. IN=wg0 OUT=enp0s3 significa que el paquete está siendo reenviado, no entregado localmente: el panel de HAProxy vive en 10.0.2.20, que es otra máquina. Y ufw deniega el reenvío por defecto — la regla allow ... to any port 8404 se aplica a la cadena de entrada, no a la de reenvío.

# ===== PASO 4: confirmar la causa raiz =====
$ grep DEFAULT_FORWARD_POLICY /etc/default/ufw
DEFAULT_FORWARD_POLICY="DROP"

Confirmado. El paso 11 de la lección se aplicó a medias: se permitió el tráfico hacia el servidor pero no el reenvío a través de él.

Resolución:

# Opcion A: permitir el reenvio en general (mas simple)
$ sudo cp /etc/default/ufw /etc/default/ufw.bak-$(date +%F)
$ sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' \
      /etc/default/ufw
$ sudo diff -u /etc/default/ufw.bak-$(date +%F) /etc/default/ufw
-DEFAULT_FORWARD_POLICY="DROP"
+DEFAULT_FORWARD_POLICY="ACCEPT"
$ sudo ufw reload

# Opcion B (preferible): reenvio SOLO lo necesario, manteniendo DROP
$ sudo ufw route allow in on wg0 out on enp0s3 to 10.0.2.20 port 8404 proto tcp
$ sudo ufw route allow in on wg0 out on enp0s3 to 10.0.2.15 port 5432 proto tcp
$ sudo ufw route allow in on wg0 out on enp0s3 to 10.0.2.0/24 port 22 proto tcp
$ sudo ufw reload

$ sudo ufw status | grep ROUTE
Anywhere on enp0s3         ALLOW FWD    Anywhere on wg0
10.0.2.20 8404/tcp         ALLOW FWD    Anywhere on wg0

La opción B es la correcta y merece defenderse: mantiene la política de lista blanca de 06-03, donde el reenvío por defecto sigue denegado y solo se permite explícitamente lo que se necesita. La opción A abre el reenvío de cualquier cosa a cualquier sitio a través del servidor VPN, que es precisamente el tipo de permiso amplio que el modelo de amenazas de 06-06 desaconseja.

Verificación:

# Desde el portatil de Marta
$ curl -sI http://10.0.2.20:8404/ | head -1
HTTP/1.1 200 OK

# Y desde el servidor, ahora hay trafico en ambos sentidos
$ sudo wg show wg0 | grep -A3 'cD5eF6gH7iJ8' | tail -1
  transfer: 128.42 KiB received, 891.20 KiB sent

El árbol de decisión resumido, que va al runbook para la próxima vez:

Síntoma Qué descarta Siguiente paso
Sin saludo Nada aún Red, puerto UDP, claves, Endpoint
Saludo, 0 B sent Red y criptografía correctas Enrutamiento o cortafuegos del servidor
Saludo, tráfico simétrico, no funciona Túnel completamente sano El servicio destino, o DNS
Va, pero se cuelga con datos grandes Todo lo anterior MTU

Y las tres lecciones de este incidente:

  1. wg show responde en un segundo la pregunta más importante: si hay saludo, el problema no es la VPN. Es el primer comando, siempre.
  2. La asimetría received / sent es un indicador muy potente que casi nadie mira: dice si el fallo está en el camino de ida o en el de vuelta.
  3. Un cortafuegos con reenvío denegado es la causa número uno de «la VPN conecta pero no llego a nada», y no aparece en ufw status de forma evidente. Los mensajes UFW BLOCK del kernel lo delatan en diez segundos.

Solución 2

Runbook: Pérdida o robo de un dispositivo con VPN

Documento: RB-SEG-04 · Versión: 1.0 · Fecha: 2026-08-18 Severidad: Alta · Tiempo objetivo de contención: 15 minutos desde el aviso Ubicación: archivador de operaciones y ~/tramontana-infra/docs/. Copia impresa: la respuesta puede tener que darse desde un móvil, sin acceso al servidor.

0. Evaluación inicial (2 min)

Tres preguntas, y no se espera a tener las respuestas para empezar el paso 1:

Pregunta Por qué importa
¿El dispositivo tiene bloqueo de pantalla y cifrado? Determina si la clave es accesible
¿Tenía guardadas otras credenciales (correo, pass, SSH)? Amplía el alcance mucho más allá de la VPN
¿Perdido u robado? Un robo dirigido implica un adversario con intención

Se asume el peor caso hasta demostrar lo contrario. Retirar el acceso de un dispositivo que luego aparece cuesta cinco minutos de reconfiguración; no retirarlo puede costar la base de datos.

1. Contención inmediata (5 min) — hacer esto ANTES de investigar

# 1.1 Identificar la clave publica del dispositivo
$ grep -B3 '10.8.0.4/32' /etc/wireguard/wg0.conf
# --- marta-movil (alta: 2026-08-18) ---
[Peer]
PublicKey = eF7gH8iJ9kL0mN1oP2qR3sT4uV5wX6yZ7aB8cD9eF0g=

# 1.2 RETIRAR EL PAR EN CALIENTE. Efecto inmediato, sin cortar a nadie mas.
$ sudo wg set wg0 peer 'eF7gH8iJ9kL0mN1oP2qR3sT4uV5wX6yZ7aB8cD9eF0g=' remove

# 1.3 Confirmar que ha desaparecido
$ sudo wg show wg0 peers | grep -c 'eF7gH8iJ9k'
0

# 1.4 Bloquear tambien la IP, por si acaso (defensa en profundidad)
$ sudo ufw insert 1 deny from 10.8.0.4 comment 'INCIDENTE 2026-08-18'

A partir de 1.2, ese dispositivo es indistinguible de un desconocido de Internet. No hay lista de revocación que propagar ni ventana de caducidad: WireGuard descarta sin responder los paquetes de claves que no conoce.

2. Persistencia del cambio (3 min)

# Sin esto, el par vuelve al reiniciar la interfaz o el servidor
$ sudo cp /etc/wireguard/wg0.conf /etc/wireguard/wg0.conf.bak-$(date +%F)
$ sudo sed -i '/--- marta-movil/,/^$/d' /etc/wireguard/wg0.conf
$ sudo diff -u /etc/wireguard/wg0.conf.bak-$(date +%F) /etc/wireguard/wg0.conf
$ sudo shred -u /etc/wireguard/clientes/marta-movil.conf

Y en el repositorio de Ansible, porque si no se quita de ahí, el siguiente ansible-playbook lo vuelve a dar de alta:

$ cd ~/tramontana-infra
$ vim roles/vpn/defaults/main.yml     # eliminar la entrada marta-movil
$ ansible-vault edit group_vars/all/vault.yml   # eliminar sus claves
$ git commit -am "Baja de marta-movil: dispositivo perdido 2026-08-18"
$ ansible-playbook sitio.yml --limit srv-tramontana --tags vpn --check --diff

3. Evaluación de alcance (15 min)

# 3.1 ¿Hubo actividad desde el momento de la perdida?
$ sudo journalctl -t wireguard --since "2026-08-18 09:00" | tail -20

# 3.2 Accesos a la base de datos desde su IP de VPN
$ sudo journalctl -u postgresql@16-main --since "2026-08-18 09:00" | \\
      grep '10.8.0.4'

# 3.3 Accesos al panel de estadisticas
$ journalctl -t nginx_acceso --since "2026-08-18 09:00" | grep '10.8.0.4'

# 3.4 Sesiones SSH
$ sudo last -i | grep '10.8.0.4'
$ sudo journalctl -u ssh --since "2026-08-18 09:00" | grep 'Accepted'
Hallazgo Conclusión Acción adicional
Sin actividad tras la pérdida Contención a tiempo Ninguna
Actividad compatible con el uso normal de Marta Probablemente ella Confirmar con ella
Actividad anómala (horas raras, consultas masivas) Compromiso probable Escalar al paso 6

4. Otras credenciales del dispositivo (20 min)

La VPN casi nunca es lo único que hay en un móvil. Se revisa y se rota todo lo que estuviera allí:

Credencial Acción Responsable
Correo corporativo Cerrar sesiones remotas y cambiar contraseña Marta
Clave SSH (si la tenía) Quitar de authorized_keys en todos los hosts Operaciones
Contraseña de PostgreSQL Rotar en pass y con ALTER ROLE Operaciones
Sesiones de Tramontana Reservas Invalidar todas las de esa cuenta Luis
Gestor de contraseñas Cambiar la clave maestra Marta
Segundo factor (TOTP) Regenerar y revisar códigos de respaldo Marta
# Ejemplo: rotar la contrasena de la base de datos
$ pass generate -f tramontana/db 32
$ sudo -u postgres psql -c "ALTER ROLE operador PASSWORD '$(pass tramontana/db)';"

5. Restablecimiento del servicio (10 min)

# Cuando Marta tenga dispositivo nuevo: par NUEVO, IP NUEVA.
# No se reutiliza ni la clave ni la direccion: reutilizar la IP
# confundiria los registros del incidente con la actividad legitima
# posterior.
$ sudo ~/scripts/alta_vpn.sh marta-movil2
[..] dando de alta 'marta-movil2' con IP 10.8.0.7

El QR se escanea presencialmente o en videollamada con Marta enseñando la pantalla del móvil nuevo. Nunca por mensajería.

6. Escalado

Se declara incidente de seguridad completo —y se sigue el procedimiento de 06-04— si se cumple cualquiera de estas condiciones:

  • Hay actividad desde la IP del dispositivo posterior al momento de la pérdida.
  • El dispositivo no tenía bloqueo de pantalla ni cifrado.
  • Contenía claves SSH o credenciales de administración.
  • Fue un robo dirigido, no un extravío.

7. Prevención: qué se cambia para la próxima vez

Esta es la parte que convierte un incidente en una mejora, y la que casi todos los procedimientos omiten.

Medida Estado Justificación
Cifrado y bloqueo obligatorios en todo dispositivo con VPN Pendiente: política a escribir Sin cifrado, la clave privada es legible extrayendo la memoria
Registrar quién tiene qué dispositivo y con qué acceso Pendiente: inventario Hoy hay que buscar en wg0.conf para saberlo
Menor privilegio por par: el móvil no necesita PostgreSQL Aplicar ya Reduce el alcance de la próxima pérdida
Alerta automática si un par nuevo o inactivo conecta Pendiente (08-06) Detección, no solo prevención
Revisión trimestral de pares y bajas Aplicar ya Los pares se acumulan y nadie los quita
Ensayo anual de este runbook Aplicar ya Un procedimiento no ensayado es una suposición
# Menor privilegio para dispositivos moviles, aplicado hoy mismo
$ sudo ufw deny from 10.8.0.7 to any port 5432 proto tcp \\
      comment 'movil: sin acceso a la BD'
$ sudo ufw deny from 10.8.0.7 to any port 22 proto tcp \\
      comment 'movil: sin SSH'

8. Registro

Se anota en el cuaderno de guardia (08-06): fecha y hora del aviso, hora de la contención, alcance evaluado, credenciales rotadas y medidas preventivas acordadas. Sin culpables: perder un móvil no es una falta, y tratarlo como tal garantiza que la próxima vez tarden dos días en avisar — que es lo único que convertiría este incidente en un desastre.

Solución 3

Sobre la VPN: qué hemos ganado y qué no cambia Para: Marta Vidal · De: Operaciones de sistemas · 18 de agosto de 2026

Respuesta breve: hemos ganado bastante, y no, no podemos simplificar nada. De hecho, la VPN funciona precisamente porque el resto de medidas siguen en su sitio.


Qué hemos montado. Un túnel cifrado que permite a tu portátil y a tu móvil comportarse, desde cualquier sitio del mundo, como si estuvieran enchufados a la red de la oficina. Necesitas una llave digital única de tu dispositivo; sin ella, el servidor ni siquiera responde: para cualquiera que lo escanee desde Internet, ese punto de entrada es indistinguible de uno que no existe.

Qué hemos ganado, en concreto:

Antes Ahora
Para ver los informes desde casa había que pedírmelo Entras directamente con tus herramientas
La base de datos era inalcanzable desde fuera Alcanzable solo por el túnel
El panel de estado, igual Igual
En el Wi-Fi de un hotel, el tráfico interno iría en claro Va cifrado de extremo a extremo
Cualquier servicio nuevo que quisieras usar desde fuera había que publicarlo Ya es accesible, sin publicar nada

Lo importante: hemos cerrado puertas, no abierto. Antes, para que pudieras trabajar desde fuera, la única opción habría sido publicar la base de datos en Internet. Con la VPN hemos hecho lo contrario: hay menos cosas expuestas que ayer, y aun así puedes llegar a más.

Ahora la pregunta que me haces, y la respuesta es que no. Entiendo por qué la haces: si la red está protegida, parece razonable relajar lo de dentro. Es un razonamiento muy extendido y es la causa de una parte importante de las brechas de seguridad que salen en las noticias.

El motivo, con un ejemplo concreto. Imagina que tu portátil se infecta con un programa malicioso —por un correo, por una web, por una memoria USB—. Ese programa está dentro de tu portátil, y tu portátil está dentro del túnel. Para él, la VPN no es un obstáculo: es una autopista directa a la base de datos de reservas. La VPN comprueba que el dispositivo es tuyo; no comprueba qué está haciendo el dispositivo.

Por eso todo lo demás sigue siendo necesario:

Medida ¿Se puede relajar? Por qué
Contraseña de la base de datos No Es lo único que para a un programa malicioso ya dentro del túnel
Cifrado de las conexiones internas No El túnel acaba en el servidor; de ahí en adelante no protege
Bloqueo de intentos de acceso repetidos No Alguien puede llegar por otras vías
Copias de seguridad No La VPN no protege de errores ni de borrados
Actualizaciones de seguridad No Un fallo en un programa se explota igual desde dentro
Registro de quién accede a qué No, al contrario Ahora hay más formas de llegar: hay que vigilar más

La forma correcta de verlo, y es como se trabaja hoy en el sector: no existe «dentro seguro» y «fuera peligroso». Cada servicio comprueba quién eres y cifra lo suyo, esté quien esté al otro lado. La VPN es una capa más, no un muro que permita bajar la guardia detrás.

Lo que sí hemos podido hacer gracias a la VPN, y esto sí es una simplificación real: restringir por persona. Como cada dispositivo tiene una dirección fija garantizada por el propio sistema —no se puede falsificar—, he podido dejar que tú llegues a la base de datos y que Luis, que es desarrollador y no la necesita en producción, no pueda. Antes eso no era posible de forma fiable. Es menos acceso, mejor repartido.

Dos cosas que necesito de tu parte:

  1. Que el móvil y el portátil tengan bloqueo de pantalla y cifrado activados. La llave digital vive en el dispositivo; si alguien lo enciende y entra sin contraseña, tiene la llave. Es la única medida que depende de ti y es la más importante de todas.
  2. Que me avises inmediatamente si pierdes un dispositivo, a cualquier hora. Retirar su acceso me lleva dos minutos y es instantáneo: no hay ventanas ni esperas. Perder un móvil no es un problema; tardar un día en decirlo, sí. Tengo el procedimiento escrito y ensayado.

Y una cosa más, para el registro. He aprovechado para cerrar al exterior la base de datos y el panel de estado, que hasta ahora dependían de estar físicamente en la red de la oficina. He dejado abierto el acceso de administración habitual, y no por descuido: si la VPN fallara tras una actualización, sin esa segunda vía tendría que desplazarme físicamente al servidor para arreglarla. Lo cerraré cuando tengamos una alternativa probada, no antes. Es la regla que aplico siempre: nunca se cierra la puerta por la que estás entrando.

Conclusión

La VPN está montada y ha pagado su coste inmediatamente: PostgreSQL y el panel de estadísticas están cerrados al exterior y, a la vez, son accesibles desde cualquier sitio. El servidor expone un puerto UDP más que ayer, y ese puerto no responde a quien no tiene clave, lo que lo hace indistinguible de uno cerrado para cualquier escáner de Internet. Es el intercambio que buscabas: menos superficie expuesta y más alcance.

Entiendes WireGuard por dentro, que era el objetivo real. Sabes que es una interfaz de red y no un demonio, y que por eso las herramientas de 06-01 y las reglas de 06-03 se aplican tal cual. Sabes que no hay servidor ni cliente en el protocolo, solo pares con claves públicas. Y sobre todo entiendes AllowedIPs, que es el concepto que separa a quien ha copiado una configuración de quien sabe lo que hace: al enviar es una tabla de rutas, al recibir es un filtro antisuplantación, y por eso en el servidor cada cliente lleva su /32 y 0.0.0.0/0 solo aparece en el lado del cliente. Ese doble papel es lo que hace que la IP de VPN de cada persona sea un identificador fiable, y lo que te ha permitido dar acceso a la base de datos a unos dispositivos y negárselo a otros.

Has decidido el túnel dividido con argumentos —ancho de banda, privacidad de las personas y el objetivo real, que es alcanzar lo interno— en lugar de por costumbre. Has verificado el cifrado capturando los mismos paquetes en las dos interfaces: opacos en enp0s3, legibles en wg0. Has automatizado el alta de clientes con wg syncconf, que aplica cambios sin cortar a nadie, y con un creates: en Ansible que impide el desastre de regenerar la clave del servidor. Y no has cerrado SSH, porque una segunda vía de acceso probada va antes que la elegancia.

Y te llevas la frase que más importa de toda la lección: una VPN no convierte una red interna en una red segura. Traslada el punto de confianza, reduce la exposición y da un identificador fiable por dispositivo. No protege de un portátil infectado, ni de una contraseña débil, ni del movimiento lateral una vez dentro. Todo lo que montaste en el Módulo 6 sigue siendo igual de necesario hoy que ayer.

En 08-05 llega el proyecto más grande del módulo y el que exige más honestidad: un clúster de Kubernetes. La lección va a decirte desde el primer párrafo que para Tramontana es desproporcionado, y va a explicarte exactamente por qué, con números. Y aun así la vas a hacer entera, porque es la tecnología dominante del sector y porque te la vas a encontrar en tu trabajo. Montarás k3s sobre srv-tramontana-pruebas, entenderás el plano de control pieza a pieza, desplegarás Tramontana Reservas con Deployments, Services, Ingress, ConfigMaps y Secrets —y verás que un Secret es base64, no cifrado, lo que enlaza directamente con 06-05—, y aprenderás a leer un CrashLoopBackOff en lugar de temerlo. Con el cierre honesto sobre la complejidad operativa real de mantener un clúster vivo.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados