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
- Qué resuelve una VPN y qué no
- Casos de uso y cuál necesita Tramontana
- WireGuard frente a OpenVPN e IPsec
- Cómo funciona WireGuard: interfaz, pares y cryptokey routing
- AllowedIPs: enrutamiento y control de acceso a la vez
- Instalación y generación de claves
- Configuración del servidor
- Configuración de los clientes: portátil y móvil
- Enrutamiento, NAT y túnel dividido frente a túnel completo
- DNS dentro del túnel
- Integración con el cortafuegos y cierre de puertos
- Verificación
- Gestión de clientes: alta, baja y rotación
- Seguridad: lo que protege y lo que no
- Automatización con Ansible
- 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:
- 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.
- 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.
- Corre en el kernel, sin copiar cada paquete a espacio de usuario. En un servidor de 2 vCPU, la diferencia se nota.
- 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.
- 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 3Las consecuencias prácticas son grandes:
- Se configura con
ipy conwg, 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
nftablesde 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
Endpointde los demás.
- 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 |
- 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,
AllowedIPsfunciona como una tabla de rutas: el paquete se cifra con la clave del par cuya lista contenga la IP de destino. - Al recibir,
AllowedIPsfunciona como un filtro antisuplantación: un paquete descifrado correctamente con la clave deluiscuya IP de origen no esté en losAllowedIPsdeluisse 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.
- 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 showindica 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 tunnelEl 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.pubLos 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/32Cuatro 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 secondsMó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.confAdvertencia 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:
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 = 25En 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 = 1El 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:
- 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.
- 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.
- 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.15Con ~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 reloadY 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/24Qué 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 conectadoCó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: wg0La 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.confPor 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
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 tcpComo 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: trueDos 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.3WireGuard 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/0solo 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-ips4. 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=1420Errores Comunes y Consejos
- Poner
AllowedIPs = 0.0.0.0/0en un par del servidor. Rompe toda su conectividad. Ahí va solo el/32del cliente. AllowedIPssolapados 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
PersistentKeepaliveen móviles. El túnel «se cae» a los pocos minutos de inactividad. - Usar
wg-quick down && uppara añadir un cliente. Corta a todos los conectados, incluido tú si administras por la VPN. Usawg syncconf. - Regenerar la clave del servidor en cada ejecución de Ansible. Deja fuera a todos los clientes de golpe.
creates:ostatantes. - 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 sentHay 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 0Tres 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=8404Ahí 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 wg0La 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 sentEl á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:
wg showresponde en un segundo la pregunta más importante: si hay saludo, el problema no es la VPN. Es el primer comando, siempre.- La asimetría
received/sentes un indicador muy potente que casi nadie mira: dice si el fallo está en el camino de ida o en el de vuelta. - 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 statusde forma evidente. Los mensajesUFW BLOCKdel 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.confY en el repositorio de Ansible, porque si no se quita de ahí, el siguiente
ansible-playbooklo 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 --diff3. 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_keysen todos los hostsOperaciones Contraseña de PostgreSQL Rotar en passy conALTER ROLEOperaciones 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.7El 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.confpara saberloMenor 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:
- 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.
- 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
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
