En un mundo donde el trabajo remoto es la norma y los recursos se distribuyen entre nubes, centros de datos y oficinas, tener una red privada segura y fácil de gestionar es más importante que nunca. Pero ¿qué pasa si quieres algo ligero, autogestionado y sin romperte la cabeza configurando Wireguard a mano? Aquí es donde entran en juego Tailscale y Headscale para crear una VPN privada gratis para tu negocio.
¿Por qué Headscale y Tailscale?
Antes de empezar con la implementación práctica, conviene repasar brevemente los conceptos clave. Tailscale es una solución de red privada virtual (VPN) basada en el protocolo WireGuard, que establece una arquitectura de malla (mesh) entre dispositivos. Por su parte, Headscale es una alternativa de código abierto que sustituye al backend propietario de Tailscale, permitiendo a los usuarios desplegar su propia infraestructura sin depender del servicio en la nube de la empresa.
Esta combinación ofrece múltiples beneficios. En primer lugar, permite una configuración prácticamente nula para el usuario final (zero-config), facilitando la adopción incluso en entornos no técnicos. Además, garantiza cifrado de extremo a extremo, una escalabilidad fluida y evita la apertura de múltiples puertos, ya que solo es necesario exponer el del servidor Headscale. Además, la autenticación de los nodos queda completamente bajo tu control.
En la siguiente figura se describe el escenario que deseamos recrear:

Para el usuario final, la única conectividad existente es el túnel VPN creado gracias a Tailscale. La conexión real es completamente transparente, por lo que nunca conocerá que su tráfico pasa a través de un servidor Headscale.
Requisitos
– Una máquina (baremetal, VM o contenedor) para Headscale
– Nodos que se unirán a la red
– Un dominio o IP fija (opcional pero recomendable)
– Docker y docker-compose
Paso 1: Despliegue y configuración de servidor Headscale
Estos pasos los debemos hacer en un equipo de nuestra empresa que disponga de IP pública y conectividad a Internet. En caso contrario, será imposible crear túneles
Creamos un archivo docker-compose.yml:
version: '3.8'
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "8080:8080"
- "3478:3478/udp"
command:
- headscale
- serve
Y un archivo config/config.yaml, que deberá estar en el mismo directorio que en el docker-compose.yml. Este fichero habrá de tener el siguiente contenido:
server_url: "http://<tu-IP-o-dominio>:8080"
listen_addr: "0.0.0.0:8080"
private_key_path: "/var/lib/headscale/private.key"
derp:
server: false
urls:
- "https://controlplane.tailscale.com/derpmap/default"
ephemeral_node_inactivity_timeout: 30m
Es muy importante escuchar en 0.0.0.0, ya que el fichero de configuración por defecto únicamente escucha en la interfaz de loopback, por lo que no aceptará clientes fuera de nuestra máquina. Si sabemos en qué interfaz queremos escuchar — la interfaz pública de nuestra máquina –, podemos especificar su IP.
Ahora, estamos a disposición de lanzar el contenedor con el siguiente comando:
docker-compose up -d
Con esto, tenemos el servidor Headscale desplegado y escuchando en el puerto 8080 de nuestra máquina :).
Paso 2: Instalación Tailscale en los clientes
La instalación de los clientes Tailscale es muy sencilla. En cada uno de los nodos que deseamos que formen parte — como clientes — de nuestra red VPN, debemos ejecutar el siguiente comando:
curl -fsSL https://tailscale.com/install.sh | sh
Con esto ya tenemos instalado el cliente Tailscale.
Paso 3: Inicialización de servidor Headscale
El siguiente paso es generar los usuarios que harán uso de nuestra VPN. Aquí hay dos aproximaciones: o creamos un usuario compartido por todos, o creamos un usuario por persona/equipo cliente. Yo prefiero la segunda aproximación, pero el resultado es análogo. Con esta segunda aproximación se puede hilar fino y definir listas de control de accesos y dotar a ciertos usuarios de más permisos. Para crear un usuario, en la máquina donde está instalado el servidor Headscale, se utiliza el siguiente comando:
docker exec -it headscale headscale users create <username>
Paso 4: Creación de usuarios
Para cada uno de los clientes, debemos crear una clave de servicio en el servidor Headscale. Para ello, se debe ejecutar el siguiente comando en la máquina que está ejecutando el servidor Headscale:
docker exec -it headscale headscale preauthkeys create --user <userID> --reusable --expiration 24h
Donde <userID> es la ID del usuario previamente creado. Para obtenerla, hacemos headscale users list y obtenemos la ID que corresponde con nuestro usuario. Si queremos que la clave de servicio no sea reutilizable (solo valga para un uso), omitimos el flag –reusable.
La clave generada con este comando deberá enviarse al cliente correspondiente, de forma segura.
Paso 5: Automatización de la creación de usuarios y claves de servicio (opcional)
A mi me gusta tener un pequeño servidor que atienda peticiones en un puerto y por debajo ejecute los comandos Headscale necesarios para la creación de un usuario y una clave de servicio. Este servidor autentica al cliente — por ejemplo, con su DNI, con clave pública-privada o con algún servicio más robusto tipo KeyCloack – y una vez comprueba que forma parte de la organización, le crea el usuario y le sirve la clave de servicio de Tailscale. De este modo nos quitamos el problema de transportar la clave. Un esqueleto de este servidor, en Python, sería algo así:
from flask import Flask, request, jsonify
import subprocess
import re
app = Flask(__name__)
# Configuración básica de usuarios autorizados -> En la vida real usar algo más robusto. Esto es un MVP
AUTHORIZED_NDIS = {
"usuario1": "NDI123456",
"usuario2": "NDI654321"
}
def crear_usuario(namespace):
try:
subprocess.run(
["docker", "exec", "headscale", "headscale", "users", "create", namespace],
check=True,
capture_output=True,
text=True
)
return f"Usuario {namespace} creado."
except subprocess.CalledProcessError as e:
if "already exists" in e.stderr:
return f"Usuario {namespace} ya existía."
return f"Error al crear usuario: {e.stderr}", 500
def obtener_user_id(namespace):
try:
resultado = subprocess.run(
["docker", "exec", "headscale", "headscale", "users", "list"],
check=True,
capture_output=True,
text=True
)
# Se parsea la salida del comando y se busca el ID del usuario
for linea in resultado.stdout.splitlines():
if namespace in linea:
# Asumimos formato tipo: "| 1 | NDI123456 | ..."
match = re.match(r'\|\s*(\d+)\s*\|\s*' + re.escape(namespace), linea)
if match:
return match.group(1)
return None
except subprocess.CalledProcessError as e:
return None
def crear_preauthkey_por_id(user_id):
try:
resultado = subprocess.run(
["docker", "exec", "headscale", "headscale", "preauthkeys", "create",
"--user", str(user_id), "--reusable", "--expiration", "24h"],
check=True,
capture_output=True,
text=True
)
return resultado.stdout
except subprocess.CalledProcessError as e:
return f"Error al crear preauthkey: {e.stderr}", 500
@app.route("/generar-clave", methods=["POST"])
def generar_clave():
data = request.json
usuario = data.get("usuario")
ndi = data.get("ndi")
if not usuario or not ndi:
return jsonify({"error": "Faltan datos (usuario o ndi)"}), 400
if AUTHORIZED_NDIS.get(usuario) != ndi:
return jsonify({"error": "NDI no autorizado"}), 403
crear_usuario(ndi)
user_id = obtener_user_id(ndi)
if not user_id:
return jsonify({"error": f"No se pudo encontrar la ID del usuario {ndi}"}), 500
salida = crear_preauthkey_por_id(user_id)
return jsonify({"resultado": salida})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8081)
El cliente Tailscale, para obtener un usuario y su respectiva clave de servicio, debería realizar la siguiente petición HTTP a http://<ip-servidor>:8081. La <ip-servidor> es la IP de la máquina de nuestra compañía que ejecuta el servidor Headscale y este pequeño servidor de Python.
curl -X POST http://localhost:8081/generar-clave \
-H "Content-Type: application/json" \
-d '{"usuario": "usuario1", "ndi": "NDI123456"}'
Este servidor, al invocarlo con nuestro DNI – si estamos autorizados – nos dará una clave de servicio para Headscale. Notar que en producción el sistema de autenticación deberá ser más robusto y se deberían limitar las peticiones por cliente para evitar la sobrecarga.
Paso 6: Unión de los clientes a la red
Un vez obtenida la clave de servicio, en el nodo cliente debemos hacer lo siguiente (sustituyendo <ip-headscale> por la IP pública del servidor que ejecuta headscale, y <clave> por la clave de servicio anteriormente obtenida).
sudo tailscale up --login-server http://<ip-headscale>:8080 --authkey <clave>
Validación
Desde un nodo, probamos el ping a otro de los nodos de la red tailscale.
Primero, con el siguiente comando, podemos obtener las IPs de todos los nodos de la red, así como la nuestra.
tailscale status
Ahora, podemos probar la conectividad con el siguiente nodo:
ping <IP-Tailscale-del-otro-nodo>
Conclusión
Con Headscale + Tailscale, en menos de una hora puedes tener una VPN robusta, cifrada y autogestionada sin dolores de cabeza. Ideal para pequeñas y medianas empresas que quieren un control total sin perder la sencillez de Tailscale.










