Headscale: autohospeda tu propio servidor de Tailscale
Instala headscale en un VPS con el paquete oficial .deb, configura server_url antes de iniciarlo y conecta tu primer nodo a una red Tailscale autogestionada.
Qué es headscale
Headscale es una implementación autohospedada del servidor de control de Tailscale. Por tanto, la máquina que coordina la red privada es un VPS que usted administra. Es un proyecto de la comunidad y no está gestionado por Tailscale Inc. Cada máquina sigue ejecutando el cliente oficial de tailscale, apuntado a su servidor con un único flag: --login-server.
El servidor de control es el componente que sabe quién pertenece a la red. Asigna a cada nodo una dirección de 100.64.0.0/10, distribuye claves públicas e indica a los nodos dónde encontrarse. Los túneles siguen usando WireGuard y se establecen de nodo a nodo. El tráfico entre dos de sus máquinas no pasa por el servidor de headscale, salvo que no se pueda establecer una ruta directa y los nodos tengan que recurrir a un relay. Si administra usted mismo esa función de coordinación, cambia quién la controla, pero no sus capacidades. Por eso conviene entender qué puede alcanzar un servidor de control y qué no en este modelo antes de considerar el cambio una mejora de seguridad por sí solo.
Cada instancia de headscale sirve un tailnet (una red de Tailscale). El proyecto lo considera adecuado para uso personal o para una organización pequeña. Con tres o cuatro máquinas, una VPN WireGuard sencilla en un VPS que usted administre implica menos software que mantener y menos componentes que puedan fallar. Headscale resulta útil cuando ya no quiere escribir manualmente un bloque [Peer] para cada portátil nuevo. El coste suele ser lo que lleva a la gente a investigar estas opciones, así que conviene leer qué incluye realmente el plan gratuito alojado antes de asumir la administración de un servidor, porque un pequeño conjunto de máquinas personales normalmente cabe dentro de sus límites. Si ya ha superado ese límite, haga los cálculos comparando el coste de los planes de pago, que se calcula por usuario y no por dispositivo, porque un hogar con una sola cuenta puede seguir pagando poco mucho después de que el número de dispositivos deje de ser relevante. Si quiere un plano de control autohospedado, pero prefiere tener su propio cliente y una interfaz web para administrar peers en lugar de un reemplazo directo de Tailscale, NetBird en un único VPS es la alternativa que conviene valorar. Para una comparación más amplia de ambos modelos, consulte en qué se diferencian WireGuard y Tailscale.
Qué necesita antes de instalar
- Un VPS con Ubuntu 24.04, una dirección IPv4 pública y acceso mediante sudo. Si el servidor es nuevo, siga primero los primeros diez minutos en un VPS nuevo.
- Un registro DNS A que apunte a esa dirección. Esta guía usa
headscale.example.com. - Un segundo dominio o subdominio para MagicDNS. Esta guía usa
tailnet.example.net. No debe ser el mismo dominio que el deserver_url. - Un equipo cliente para añadir, con Linux, macOS, Windows, Android o iOS.
Instalar headscale desde el paquete .deb oficial
El proyecto publica paquetes .deb en su página de versiones de GitHub. En julio de 2026, la versión actual es 0.29.3. Compruebe primero la arquitectura, porque el nombre del archivo la incluye.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureEn un VPS x86 normal, ese comando muestra amd64 y, en un plan basado en Ampere o Graviton, arm64. Introduzca el resultado en la variable siguiente.
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale versionEl ./ delante del nombre del archivo es obligatorio. Sin él, apt busca en sus repositorios un paquete llamado headscale.deb y falla.
El paquete crea un usuario del sistema headscale, escribe un archivo de configuración predeterminado /etc/headscale/config.yaml e instala una unidad de systemd. No inicia el servicio, y ese es el orden correcto. La configuración incluida apunta server_url a http://127.0.0.1:8080, que no es una dirección accesible para ninguno de sus clientes. Por tanto, un servicio iniciado ahora estaría mal configurado aunque llegara a arrancar. Ejecutar sudo systemctl is-active headscale en este punto muestra inactive. Es lo esperado, no un error.
Configure server_url antes de iniciar el servicio
Edite /etc/headscale/config.yaml con sudo nano /etc/headscale/config.yaml o aplique los mismos tres cambios con sed. Conserve una copia del original, porque el archivo es largo y contiene muchos comentarios. Es la mejor referencia disponible para el resto de los ajustes.
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url es la dirección que headscale escribe en cada registro de cliente. Después, los clientes se conectan siempre a esa cadena exacta. Por tanto, debe ser el nombre público con https:// delante, nunca 127.0.0.1.
listen_addr indica dónde se enlaza el proceso. Déjelo en loopback. Un proxy inverso en el mismo servidor termina TLS (seguridad de la capa de transporte) y reenvía las conexiones a ese proceso. Por tanto, nada externo al servidor necesita acceder al puerto 8080.
base_domain es el sufijo de MagicDNS, el dominio bajo el que los nodos reciben sus nombres. Debe ser un nombre de dominio completo sin un punto final. También debe ser un dominio diferente del indicado en server_url, porque de lo contrario los dos espacios de nombres entrarían en conflicto.
Deje intacta la sección de la base de datos. El valor predeterminado es SQLite en /var/lib/headscale/db.sqlite, dentro de un directorio que creó y administra el paquete. SQLite es suficiente para un tailnet de este tamaño.
Inicie headscale y confirme que está funcionando
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active muestra active y curl muestra 200. enable --now hace las dos partes del trabajo: inicia el servicio y lo marca para que se inicie después de un reinicio.
Si is-active muestra failed, consulte el registro con sudo journalctl -u headscale -n 50 --no-pager. En esta fase, el fallo casi siempre se debe al archivo de configuración, porque headscale analiza todo el archivo antes de abrir un socket. Por eso, una sangría incorrecta o una clave desconocida detiene el proceso antes de que empiece a escuchar. Corrija el archivo y después ejecute sudo systemctl restart headscale. Todos los cambios de configuración posteriores requieren el mismo reinicio. Los clientes se vuelven a conectar automáticamente. Si systemd es nuevo para usted, ejecutar sus propios servicios y temporizadores con systemd explica los comandos usados aquí.
Compruebe los archivos de estado mientras está en el shell:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyLas dos líneas comienzan con headscale, el usuario sin privilegios que creó el paquete. noise_private.key es la identidad del servidor para sus clientes. Consérvelo. Si lo elimina, headscale generará uno nuevo y todos los nodos tendrán que registrarse de nuevo.
Configurar TLS delante de headscale
Los clientes deben acceder a server_url mediante HTTPS. Caddy es la opción más sencilla porque solicita y renueva el certificado automáticamente.
sudo apt install -y caddySustituya /etc/caddy/Caddyfile por el bloque de la documentación de headscale:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddyvalidate muestra adapted config to JSON cuando el archivo se analiza correctamente. Un aviso que indique que el archivo no está formateado sólo es cosmético. Desde su portátil, curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health también debería mostrar 200. Esta comprobación confirma que DNS, el firewall, el certificado y el proxy funcionan conjuntamente.
Este es el detalle del proxy que suele hacer perder una tarde. La conexión de control de Tailscale es una actualización HTTP, se inicia con POST en lugar de GET y el valor de la cabecera Upgrade es tailscale-control-protocol. Caddy la reenvía sin configuración adicional. nginx no lo hace, por lo que un front end de nginx necesita el siguiente mapa de actualización:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}Si omite esas líneas, las peticiones normales siguen funcionando. Por eso /health devuelve 200 y todo parece correcto, mientras la conexión de control de larga duración nunca se establece y los nodos se registran y después aparecen sin conexión. Si elige nginx, Certbot en Ubuntu 24.04 con nginx explica la parte de los certificados.
Qué puertos abrir en UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseEl puerto 443 transporta todas las comunicaciones de los clientes. El puerto 80 sólo se usa para el desafío HTTP de ACME (entorno de gestión automática de certificados) y para la redirección a HTTPS. Caddy también lo necesita para obtener un certificado.
El puerto 8080 permanece cerrado. listen_addr es 127.0.0.1:8080, por lo que el proxy llega a headscale mediante la interfaz de loopback y no interviene ninguna regla del firewall. Abrir el puerto 8080 a Internet proporciona a los clientes un canal de control en texto claro y no aporta ninguna ventaja. Tenga en cuenta que la mayoría de los proveedores ejecutan un segundo firewall en su panel de control, independiente de UFW. Por eso, un puerto puede estar abierto en el servidor y seguir cerrado en el perímetro. Conceptos básicos del firewall UFW en un VPS explica con más detalle la sintaxis de las reglas.
Cree un usuario y una clave de preautenticación
sudo headscale users create alice
sudo headscale users listEl comando headscale es un cliente. Se comunica con el daemon en ejecución mediante el socket Unix ubicado en /var/run/headscale/headscale.sock, que tiene el modo 0770 y pertenece al grupo headscale. Esto tiene dos consecuencias. El comando falla mientras el servicio está detenido, que es otra razón por la que importa el orden de este procedimiento, y necesita sudo, a menos que añada su propia cuenta al grupo headscale.
users list muestra un ID junto a cada nombre. Necesita ese número porque el comando de claves acepta un ID de usuario numérico, no un nombre.
sudo headscale preauthkeys create --user 1 --expiration 24hLa clave se muestra una sola vez. Cópiela ahora. Una clave de preautenticación se puede usar una sola vez y es válida durante una hora, salvo que indique lo contrario. Por eso conviene establecer --expiration 24h mientras todavía realiza pruebas. Añada --reusable para crear una clave que permita registrar varias máquinas y trátela como una contraseña, porque cualquiera que la tenga puede unirse a su red.
Conecte su primer cliente con --login-server
En la máquina que quiere añadir:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 muestra la dirección que headscale asignó, algo parecido a 100.64.0.1. De vuelta en el servidor, sudo headscale nodes list muestra el nodo con su ID, su usuario y su estado de conexión.
El valor de --login-server debe coincidir exactamente con server_url, incluido el esquema y sin una barra final. Se comparan como cadenas. Si no coinciden, el cliente se registra con una dirección y después recibe instrucciones para comunicarse con otra.
Una máquina que ya había iniciado sesión en el servicio alojado de Tailscale conserva esa sesión. Ejecute primero sudo tailscale logout y después tailscale up con --login-server.
Si omite --auth-key, el cliente muestra una URL. Ábrala. La página muestra el identificador de ese intento de registro, que debe aprobar en el servidor:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEEste formulario resulta más cómodo para su propio portátil. Las claves de preautorización son mejores para cualquier proceso automatizado, porque no es necesario que una persona supervise el proceso. Una vez que el propio VPS es un nodo, también puede transportar el tráfico de Internet de sus otras máquinas. Esa es la configuración del nodo de salida, con la diferencia de que debe aprobar la ruta anunciada en el servidor mediante el comando headscale, en lugar de hacerlo en una consola de administración alojada. Si lo que necesita es acceder a una red privada situada detrás de ese VPS, en vez de disponer de una salida a Internet, el mismo paso de aprobación permite anunciar esa subred al resto de su tailnet. Publicar una aplicación desde un nodo, en lugar de encaminar redes completas a través de él, es otra tarea. serve y funnel son las dos formas de hacerlo, aunque ambas dependen de los certificados y del sistema de entrada propios de Tailscale. Considérelas funciones de un tailnet alojado, no algo que headscale proporcione directamente.
DERP y qué retransmite el tráfico cuando falla una ruta directa
DERP (designated encrypted relay for packets) es la ruta de respaldo. Cuando dos nodos no pueden establecer una conexión directa de WireGuard, normalmente porque ambos están detrás de una NAT (network address translation) estricta, envían los paquetes a través de un relay. El relay no tiene ninguna clave, por lo que no puede leer el tráfico. Sí puede ver qué nodos se comunican y cuántos datos transfieren.
Debe quedar claro qué hace la configuración predeterminada. Headscale se distribuye apuntando a https://controlplane.tailscale.com/derpmap/default con auto_update_enabled: true y update_frequency: 3h, por lo que el plano de control es suyo, pero los relays son de Tailscale. Para la mayoría de los usuarios, es una compensación razonable. Si no lo es, ejecute su propio relay.
Para ejecutar su propio relay, establezca enabled: true dentro de derp.server en config.yaml, reinicie headscale y abra el puerto STUN (session traversal utilities for NAT) con sudo ufw allow 3478/udp. El archivo de configuración indica claramente el requisito: server_url debe usar https, porque DERP requiere TLS. Vaciar la lista derp.urls elimina los relays de Tailscale del mapa. Si lo hace sin un relay integrado que funcione, cualquier par de nodos que no pueda conectarse directamente no podrá conectarse en absoluto.
Desde un cliente, tailscale netcheck muestra la latencia hacia cada región de retransmisión que conoce, y tailscale status marca cada par como direct con una dirección o relay con un código de región. Un par atascado en relay indica un problema de NAT, no un problema de headscale. Un par que está direct y sigue siendo lento plantea otra cuestión, y la respuesta habitual en ese caso es MTU, no el túnel en sí.
¿Por qué un nodo aparece sin conexión?
El proxy está descartando la actualización. Este es el caso más habitual. La señal característica es que todo lo demás parece correcto: /health devuelve 200, headscale nodes list muestra el nodo y el nodo nunca se conecta. La conexión de control es un POST que contiene Upgrade: tailscale-control-protocol. Un proxy que no lo reenvía elimina el único canal que informa del estado del nodo. Compare la configuración de nginx con el bloque map anterior o cambie a Caddy para descartar el proxy.
server_url cambió después de registrar los nodos. Los nodos siguen conectándose al valor que recibieron durante el registro. Si lo editó, ejecute sudo tailscale up --login-server https://headscale.example.com --force-reauth en cada nodo.
El cliente no está en ejecución. En el nodo, ejecute sudo systemctl is-active tailscaled y sudo journalctl -u tailscaled -n 50 --no-pager. Un cliente que no puede resolver o alcanzar su dominio registra allí sus reintentos.
La clave ha caducado. Se explica en la sección siguiente.
Para supervisar el lado del servidor durante las pruebas, ejecute sudo journalctl -u headscale -f en el VPS y reinicie tailscaled en el cliente. Un nodo que alcanza headscale produce líneas de registro de inmediato. Si no aparece nada, la solicitud no está llegando. Revise DNS, el firewall y el proxy antes de revisar headscale.
Expiración de claves y el nodo que deja de funcionar semanas después
Existen dos expiraciones independientes. Confundirlas hace perder tiempo.
Las claves de preautenticación caducan rápidamente de forma intencionada. El valor predeterminado es una hora y un uso. Si tailscale up rechaza la clave, genere una nueva en el servidor en lugar de editar nada en el cliente.
Las claves de nodo son la parte de larga duración. La sección node de config.yaml configura expiry: 0, y 0 significa que no hay una expiración predeterminada: un nodo registrado sigue siendo válido hasta que lo hace caducar. Los nodos etiquetados nunca caducan. Configure expiry: 180d si quiere que los registros caduquen automáticamente y tenga en cuenta lo que implica: todos los nodos que no estén etiquetados necesitarán sudo tailscale up --login-server https://headscale.example.com --force-reauth según ese intervalo, y un servidor sin interfaz al que nadie vuelva a autenticar perderá por sí solo el acceso a la red.
Hágalo manualmente cuando alguien pierda un portátil. sudo headscale nodes list le proporciona el ID; después, sudo headscale nodes expire -i 3 cierra la sesión de ese nodo y sudo headscale nodes delete -i 3 lo elimina completamente de la red.
Copias de seguridad y actualizaciones
/var/lib/headscale y /etc/headscale forman todo el servidor. Detenga el servicio antes de copiarlos, porque SQLite puede tener escrituras pendientes y una base de datos copiada bajo carga puede quedar incoherente.
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgzCopie ambos archivos fuera del servidor. Contienen las claves privadas y todos los registros, por lo que requieren el mismo nivel de protección que el propio servidor. copias de seguridad con restic desde un VPS explica cómo hacerlo de forma programada y cifrada.
Las actualizaciones repiten la instalación: descargue el nuevo .deb, sudo apt install ./headscale.deb, reinicie y vuelva a ejecutar las comprobaciones is-active y /health. Desde la versión 0.29, la ruta de actualización es estricta. No se permite omitir una versión menor ni volver a una versión menor anterior. Avance una versión menor cada vez, haga una copia de seguridad antes de cada paso y lea primero las notas de la versión correspondiente, porque esa versión modificó el comportamiento de la política ACL y trasladó varias claves de configuración.
FAQ
¿Por qué headscale no se inicia justo después de instalar el archivo .deb?
El paquete instala la unidad, pero deja el servicio detenido, y el /etc/headscale/config.yaml predeterminado es una plantilla, no una configuración funcional. Edite primero server_url, listen_addr y base_domain. Después, ejecute sudo systemctl enable --now headscale y confirme con sudo systemctl is-active headscale. Si sigue fallando, sudo journalctl -u headscale -n 50 --no-pager indica el problema. En este punto, casi siempre se trata de un error de YAML, porque headscale analiza todo el archivo antes de enlazar un puerto.
¿Tengo que instalar el cliente normal de Tailscale en mis equipos?
Sí. Headscale sólo reemplaza el servidor de control. Cada nodo ejecuta el cliente oficial de Tailscale, y debe indicarle el servidor con sudo tailscale up --login-server https://headscale.example.com. Esa opción existe en el cliente estándar, por lo que no es necesario modificarlo ni recompilarlo.
¿Mi tráfico pasa por el servidor headscale?
Por lo general, no. Headscale coordina la red y distribuye claves y direcciones, mientras que la ruta de datos usa WireGuard directamente entre los nodos. El tráfico sólo toma un desvío cuando dos nodos no pueden comunicarse directamente y recurren a un relay DERP. Con la configuración incluida, esos relays son los públicos de Tailscale. Ejecute tailscale status en un nodo para comprobar si un peer está direct o en un relay.
¿Por qué mi nodo permanece desconectado después de registrarse?
Un nodo que aparece en headscale nodes list pero nunca pasa a estar conectado normalmente ha perdido la conexión de control en el reverse proxy. Esa conexión es una actualización HTTP enviada como POST con la cabecera Upgrade: tailscale-control-protocol, y nginx la descarta si no añade el bloque map $http_upgrade $connection_upgrade y las líneas proxy_set_header correspondientes. Caddy la reenvía sin configuración adicional, por lo que es una forma rápida de comprobar si el problema está en el proxy.
¿Necesito un nombre de dominio y TLS para headscale?
En la práctica, sí. Los clientes se conectan a la cadena que indique en server_url, los certificados se emiten para nombres y no para direcciones IP sin nombre, y el archivo de configuración indica que DERP requiere TLS. Un dominio y Caddy permiten disponer de un endpoint HTTPS en unos cinco minutos, con renovación automática. Ejecutar el servidor de control mediante HTTP sin cifrar significa que toda comunicación de los clientes con él atraviesa Internet sin cifrado.