---
title: Configura tu VPN Mesh con Headscale: guía completa para sysadmins Linux
url: https://soylinux.com/configura-vpn-mesh-headscale-guia-sysadmins-linux-2026/
date: 2026-11-07T05:21:04-05:00
modified: 2026-11-07T05:21:04-05:00
author: Alfonso Moreno | Sysadmin Senior
description: Malla VPN con Headscale 0.29.7: tags con tagOwners, policy HuJSON con grants, subnet routers, exit nodes y alta disponibilidad.
image: https://soylinux.com/wp-content/uploads/2026/11/post_9577_featured-1.jpg
categories: Redes
tags: headscale, linux, networking, redes, sysadmin, tailscale, vpn
language: es
site: Soy Linux
---

Tienes tres nodos en un homelab, un portátil y una máquina en la nube, y quieres que se hablen entre sí sin abrir un solo puerto en el router. Instalar el servidor de control es la parte fácil: eso ya lo puedes leer en [la guía de Headscale que publicamos en agosto](https://soylinux.com/headscale-tu-propio-tailscale-self-hosted-2026-2/). Lo difícil aparece después, cuando la malla deja de ser “el portátil puede ver el NAS” y empieza a ser una red con reglas: quién puede hablar con quién, qué nodos exponen rutas hacia la LAN, cuál es el nodo de salida, y qué pasa cuando uno de esos nodos se cae a la mitad del día.

Esta guía es la segunda parte. Vamos a montar **una malla con segmentación real** sobre Headscale: usuarios y tags con reglas de propiedad, una `policy` en formato HuJSON con *grants* en vez de ACLs legacy, subnet routers con aprobación de rutas, exit nodes, respaldo para la alta disponibilidad y el ciclo de vida de los nodos. Todo contra **Headscale 0.29.7**, la versión que salió el 30 de octubre de 2026 y que es la que debes instalar hoy.

TL;DR — Lo que vas a tener al final

- **Tiempo de setup:** 45-60 minutos sobre el Headscale 0.29.7 que ya tienes instalado, más 15 minutos extra por cada subnet router que registres con `--advertise-routes` y apruebes con `headscale nodes approve-routes`.
- **Requisitos:** un servidor de control Headscale 0.29.7 con su `policy` en HuJSON, el cliente Tailscale 1.104.4 o superior en cada nodo, y una key por rol creada con `headscale preauthkeys create --acl-tags` para que cada servicio nazca con su identidad desde el registro.
- **Resultado:** una malla donde los servidores de infraestructura son *tagged nodes* con dueño declarado en `tagOwners`, los dispositivos personales siguen atados a su usuario, y las rutas hacia tu LAN solo se activan cuando el servidor de control las aprueba explícitamente. Eso es segmentación real, no una ACL de adorno.
- **Troubleshooting:** si un nodo anuncia su ruta y los demás no la ven, el problema casi nunca es Tailscale: revisa `headscale nodes list-routes` y mira la columna Approved, porque la ruta puede estar Available y aun así sin aprobar. Y si un tag no se aplica, el owner no estaba declarado en `tagOwners` al momento del registro.

## Por qué 0.29.7 y no la que tenías instalada

Antes de tocar la política conviene fijar la base, porque la versión 0.29.x reorganizó varias cosas y las guías anteriores quedaron desactualizadas en puntos que afectan directamente a lo que vas a montar.

La serie 0.29.7 es la que está en pie: se publicó el **30 de octubre de 2026** y es una *release de seguridad*. Corige hallazgos del escáner de Anthropic (`oss-scanner`) y del informe Tailworm, ninguno crítico pero todos capaces de provocar denegación de servicio. La nota de publicación es explícita: actualiza a Headscale 0.29.7 **y** a Tailscale 1.104.4 o superior, sobre todo si usas Tailscale SSH.

Ese “y” importa. Las vulnerabilidades de reenvío SSH, manejo de sesiones SSH y `Serve` viven en el daemon `tailscaled` de cada nodo, no en el servidor de control. Actualizar Headscale no las arregla: hay que actualizar el cliente en cada máquina.

Si necesitas el rodeo inmediato, el propio release lo documenta. La variable de entorno `TS_SSH_DISABLE_FORWARDING=1` en el servicio `tailscaled` desactiva el agente SSH y el reenvío de puertos en todas las sesiones SSH de Tailscale de ese nodo:

```bash
# Verificar la versión del servidor de control
headscale version

# Verificar la versión del cliente en CADA nodo
tailscale version

# Endurecer un nodo mientras actualizas la flota (0.29.7 lo recomienda)
sudo systemctl edit tailscaled

```

En el override de `tailscaled` agrega el bloque de entorno:

```ini
[Service]
Environment=TS_SSH_DISABLE_FORWARDING=1

```

Recarga y reinicia el daemon para que tome el cambio:

```bash
sudo systemctl daemon-reload
sudo systemctl restart tailscaled

```

El mínimo de cliente que declara la 0.29.7 es **Tailscale 1.80.0**. Antes de migrar una flota existente, corre `tailscale version` en cada nodo y actualiza los que estén por debajo de esa línea.

## El modelo de identidad: por qué la etiqueta no es un alias

Acá está el punto que más confunde a quien viene de Tailscale SaaS, y la razón por la que una malla bien segmentada se comporta de forma distinta a un grupo plano de nodos.

Headscale implementa el modelo de identidad de Tailscale, que distingue dos tipos de nodo:

- **Nodo personal**: pertenece a un usuario humano. Es tu portátil, tu teléfono, tu estación de trabajo. Cada nodo personal tiene un dueño.
- **Nodo etiquetado** (*tagged node*): pertenece a una etiqueta, no a una persona. Es un servidor web, una base de datos, un router. Los nodos etiquetados se agrupan bajo el usuario especial `tagged-devices` y los administra un equipo, no un individuo.

Esa distinción no es decorativa: un nodo etiquetado **no puede iniciar Tailscale SSH contra un nodo personal**. Es una restricción deliberada, porque los nodos etiquetados suelen ser servicios compartidos y no deberían ser un punto de entrada hacia las máquinas de una persona concreta.

La consecuencia práctica de la segmentación es esta: las ACLs y los grants que escribas van a identificar fuentes por `tag:`, no por usuario, cuando el destino sea infraestructura. Si dejas los servidores como nodos personales, cada regla tiene que nombrar al dueño, y el día que ese dueño se va de la empresa el acceso se rompe o queda abierto. Con tags, la regla dice “lo que tenga `tag:server` puede hablar con lo que tenga `tag:db`“, y el dueño deja de importar.

## Crear la estructura de tags y sus dueños

Los tags se declaran en dos lugares distintos que se complementan: la `policy` dice *quién puede asignar cada tag*, y la CLI asigna los tags a los nodos concretos. Si saltas el primer paso, cualquier nodo que se registre con un tag puede reclamarse una identidad que no le corresponde.

Antes de nada, la política base con `tagOwners`. Este es el fragmento que va a gobiernar todo lo demás:

```json
# /etc/headscale/policy.json
{
  "tagOwners": {
    "tag:server": ["roger@", "equipo@"],
    "tag:db": ["equipo@"],
    "tag:router": ["equipo@"],
    "tag:laptop": ["roger@"]
  }
}

```

Los dos pasos siguientes son obligatorios en cualquier despliegue real:

1. Declarar el tag en `tagOwners` dentro de la política, diciendo qué usuarios o grupos pueden asignarlo.
2. Validar la política **antes** de cargarla, y recargar el servidor para que tome los cambios.

La validación existe como comando propio desde la CLI, y funciona contra el archivo sin necesidad de tocar el servicio:

```bash
# Validar la política contra la base de datos, sin gRPC y sin servidor corriendo
sudo headscale policy check --file /etc/headscale/policy.json \
  --bypass-grpc-and-access-database-directly

# Cargar la política
sudo headscale policy set --file /etc/headscale/policy.json

# Recargar para que Headscale la tome en cuenta
sudo systemctl reload headscale

```

Ese `--bypass-grpc-and-access-database-directly` es lo que permite resolver las referencias a usuarios y evaluar los bloques de pruebas contra la base de datos real. Sin él, la validación de usuarios queda incompleta.

La política vive en un archivo y Headscale la recarga con `systemctl reload headscale` o con una señal `SIGHUP` al proceso. El resultado del procesamiento queda en el log, así que después de cada cambio conviene mirarlo:

```bash
sudo journalctl -u headscale -n 50 --no-pager

```

Ahora sí, asignar los tags a los nodos. El comando es `headscale nodes tag`, con el identificador del nodo y la lista de tags separada por comas:

```bash
# Ver los nodos y los tags que tienen asignados
sudo headscale nodes list

# Convertir un nodo personal en etiquetado con dos tags
sudo headscale nodes tag -i 1 -t tag:server,tag:prod

```

Al aplicar un tag, el nodo deja de pertenecer al usuario y pasa al usuario especial `tagged-devices`. Eso significa que un nodo no puede tener a la vez un dueño humano y tags: son mutuamente excluyentes. Si necesitas devolver un nodo etiquetado a un usuario personal, hay que re-autenticarlo:

```bash
sudo tailscale up --login-server https://vpn.tudominio.com \
  --advertise-tags= --force-reauth

```

Ese flujo abre el navegador para completar el registro y muestra el Auth ID que hay que aprobar en el servidor:

```bash
sudo headscale auth register --user roger --auth-id EL_AUTH_ID

```

Para nodos de servicio, conviene crearlos directamente con el tag desde el registro, en vez de convertirlos después. Una pre-auth key con `--tags` asigna la identidad en el momento del registro, que es exactamente lo que quieres en un despliegue automatizado:

```bash
# Crear una pre-auth key de un solo uso, con expiración corta y tag
sudo headscale preauthkeys create --user 1 --expiration 30m --tags tag:server

# Listar las keys vigentes
sudo headscale preauthkeys list

# Retirar una key antes de que expire sola
sudo headscale preauthkeys expire --id 3

```

Sobre los flags de `preauthkeys create`, que es donde más se equivoca la gente: `--reusable` y `--ephemeral` son booleanos, `--expiration` acepta valores legibles como `30m`, `24h` o `99y`, y `--tags` recibe la lista de tags. La expiración por defecto de la CLI es de **1 hora** y la key es de un solo uso salvo que pases `--reusable`.

## La política: grants en vez de ACLs

Acá viene el cambio de fondo en la versión 0.29, y es la razón principal por la que escribo esta segunda guía en lugar de repetir la instalación.

Headscale implementa buena parte del sistema de políticas de Tailscale: tanto ACLs como *grants*, y Tailscale SSH. Pero la documentación oficial es clara en cuál de los dos usar: **recomienda `grants`**, porque las ACLs se consideran heredadas y no van a recibir nuevas funciones de Tailscale. Si empiezas una política hoy con `acls:`, vas a tener que migrarla después.

El formato del archivo es [HuJSON](https://github.com/tailscale/hujson), que es JSON con comentarios y comas opcionales. El ejemplo mínimo que bloquea todo el tráfico:

```json
# /etc/headscale/policy.json
{
  "grants": []
}

```

Y el que abre todo, que es el comportamiento por defecto cuando no hay política cargada: si defines un archivo de política pero omites la sección `grants`, Headscale permite todo el tráfico entre nodos. Conviene ser explícito y no confiar en ese default.

Una política de tres planos que se parece a lo que vas a querer: los nodos personales se hablan entre sí, los servidores con tag aceptan SSH solo desde los nodos de infraestructura, la base de datos solo acepta tráfico de los servidores de aplicación, y nadie accede al router directamente salvo el equipo.

```json
# /etc/headscale/policy.json
{
  "tagOwners": {
    "tag:server": ["equipo@"],
    "tag:db": ["equipo@"],
    "tag:router": ["equipo@"],
    "tag:laptop": ["equipo@"]
  },
  "grants": [
    {
      "src": ["autogroup:member"],
      "dst": ["autogroup:self"],
      "ip": ["*"]
    },
    {
      "src": ["tag:server"],
      "dst": ["tag:db:5432"],
      "ip": ["*"]
    },
    {
      "src": ["autogroup:member"],
      "dst": ["tag:server:22"],
      "ip": ["*"]
    },
    {
      "src": ["tag:server"],
      "dst": ["autogroup:internet"],
      "ip": ["*"]
    }
  ]
}

```

Los `autogroups` son los atajos que hacen mantenible todo esto. Headscale soporta un subconjunto de los de Tailscale, y vale la pena conocerlos porque ahorran muchísimo:

AutogroupQué incluyeRestricción`autogroup:member`Todos los dispositivos personales, sin tagsNinguna`autogroup:tagged`Todos los dispositivos que tienen al menos un tagNinguna`autogroup:self`Dispositivos donde el mismo usuario está autenticado en origen y destinoSolo como destino`autogroup:internet`Acceso a internet a través de exit nodesSolo como destino`autogroup:nonroot`Cualquier usuario excepto rootSolo en reglas SSH`autogroup:danger-all`Todas las IP, incluidas las fuera de los rangos de TailscaleSolo como origen

Un detalle sobre `autogroup:self` que conviene saber antes de producción: la implementación actual es ineficiente. Headscale tiene que compilar las reglas de filtrado por nodo en lugar de globalmente, y en despliegues grandes eso degrada el rendimiento del servidor coordinador. Si lo usas y notas que el servidor se pone lento, cámbialo por reglas explícitas por usuario.

Dos limitaciones de la implementación de Headscale que aparece en la documentación oficial y que evitan sorpresas: las *device postures* y el campo `srcPosture` no están soportados, y los *IP sets* tampoco. Si tu política de Tailscale SaaS depende de posture checks para decidir acceso, eso hay que resolverlo de otra forma al migrar.

## Subnet routers: el doble opt-in

Un subnet router es el nodo que anuncia rutas hacia una red que no está en la malla, típicamente tu LAN `192.168.1.0/24`. La parte que casi todos pasan por alto es que **anunciar no es aprobar**: hay un opt-in doble, uno del lado del router y otro del lado del servidor de control.

El router anuncia sus rutas en el cliente:

```bash
# En el nodo que va a ser subnet router
sudo tailscale up --login-server https://vpn.tudominio.com \
  --advertise-routes=192.168.1.0/24

# Si el nodo ya estaba registrado, cambia las rutas sin reconectar
sudo tailscale set --advertise-routes=192.168.1.0/24

```

Y el servidor de control las aprueba explícitamente. Primero miras qué se anunció:

```bash
sudo headscale nodes list-routes

```

La salida muestra cinco columnas que importan: el ID del nodo, su hostname, las rutas **Approved**, las **Available** y las **Serving (Primary)**. Una ruta puede estar disponible y aun así no aprobada, que es exactamente el caso que te va a confundir si solo miras el cliente.

Después, la aprobación con las rutas exactas, separadas por coma:

```bash
sudo headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24

```

Para que el router sea alcanzable, necesita IP forwarding activo en el host:

```bash
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

```

En el lado del cliente, el nodo que quiere alcanzar la LAN tiene que aceptar las rutas anunciadas. El flag `--accept-routes` está activo por defecto en Linux, pero conviene ser explícito si vienes de una configuración previa:

```bash
sudo tailscale set --accept-routes=true

```

### Aprobación automática con autoApprovers

Si tienes varios routers o los re-creas seguido, aprobar rutas a mano se vuelve una tarea recurrente. La sección `autoApprovers` de la política delega esa decisión: cualquier subnet router que anuncie un tag específico auto-aprueba sus rutas sin intervención del administrador.

```json
# /etc/headscale/policy.json
{
  "tagOwners": {
    "tag:router": ["equipo@"]
  },
  "autoApprovers": {
    "routes": {
      "192.168.1.0/24": ["tag:router"],
      "10.8.0.0/16": ["tag:router"]
    }
  },
  "grants": []
}

```

Y el router se registra anunciando el tag y las rutas a la vez:

```bash
sudo tailscale up --login-server https://vpn.tudominio.com \
  --advertise-tags tag:router \
  --advertise-routes 192.168.1.0/24

```

El tag del router es lo que da derecho a la aprobación automática. Si un nodo anuncia las rutas pero no el tag, la ruta queda pendiente de aprobación manual: el mecanismo es deliberado, para que un nodo cualquiera no se auto-otorgue acceso a tu LAN.

## Alta disponibilidad: dos routers, una ruta

El 0.29.x trae un detalle de la configuración que resuelve el peor problema operativo de los subnet routers: qué pasa cuando el router principal se cae. Headscale puede hacer *health probing* de los routers que anuncian el mismo prefijo.

La lógica está en `node.routes.ha` dentro del archivo de configuración:

```yaml
# /etc/headscale/config.yaml
node:
  routes:
    ha:
      # Cada cuánto se hace ping al router vía el canal Noise
      probe_interval: 10s
      # Cuánto se espera la respuesta antes de marcarlo como no saludable
      probe_timeout: 5s

```

El comportamiento es el siguiente: si hay dos o más nodos anunciando el mismo prefijo, Headscale les hace ping a cada uno por el canal Noise. Si uno no responde dentro del timeout, se lo marca como no saludable y el rol primario pasa al siguiente que esté sano. Cuando el nodo caído vuelve a responder, vuelve a estar sano pero **no recupera el rol primario**, para evitar el aleteo entre los dos.

El tiempo máximo de detección es la suma de ambos valores, unos 15 segundos con los valores por defecto. Los mínimos son explícitos en la documentación: `probe_interval` tiene que ser de 2 segundos o más, y `probe_timeout` de 1 segundo o más pero menor que el intervalo. Poner `probe_interval: 0` desactiva el probing, y el mecanismo es un no-op cuando no hay rutas HA declarada.

La parte importante del diseño es que el probing no usa WireGuard sino el canal Noise, que es el canal de control cifrado contra el servidor. Por eso funciona incluso cuando el problema de red es justamente la ruta que el router anuncia.

## Exit node: controlar por dónde sale el tráfico

El exit node es el nodo por el que sale todo el tráfico a internet de la malla. Al igual que el subnet router, requiere doble opt-in: el nodo se ofrece y el servidor de control aprueba.

El nodo se ofrece como exit node y activa el reenvío de IP:

```bash
# Ofrecerse como exit node
sudo tailscale up --login-server https://vpn.tudominio.com --advertise-exit-node

# O, si ya está registrado
sudo tailscale set --advertise-exit-node

# IP forwarding en el host del exit node
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

```

Los exit nodes se reconocen en la lista de rutas por anunciar `0.0.0.0/0` en IPv4 y `::/0` en IPv6. Se listan con el mismo comando que las demás:

```bash
sudo headscale nodes list-routes

```

Y se aprueban por la misma vía. Para exit nodes alcanza con aprobar una de las dos rutas: la otra se aprueba automáticamente:

```bash
sudo headscale nodes approve-routes --identifier 2 --routes 0.0.0.0/0

```

En el cliente, usar el exit node es un flag aparte. Un detalle que confunde: `--exit-node` es para *consumir* un exit node, y `--advertise-exit-node` es para *ofrecer* uno. No son intercambiables:

```bash
# Usar el exit node del servidor (nombre o IP)
sudo tailscale set --exit-node=nombre-del-exit-node

# Volver a salir directo por la conexión local
sudo tailscale set --exit-node=

```

Y verificas que realmente está saliendo por ahí, no por tu café:

```bash
curl -s ifconfig.me
echo

```

La política puede restringir quién usa el exit node con el autogroup `autogroup:internet`, que solo puede aparecer como destino. Eso permite que los servidores de producción salgan por el exit node corporativo mientras los portátiles de desarrollo salen por su propia conexión.

## DERP: el relay que decide tu latencia

Cuando dos nodos no pueden establecer una conexión directa de WireGuard, el tráfico viaja cifrado a través de un relay DERP sobre HTTPS, en el puerto 443. Eso es lo que hace que la malla funcione detrás de CGNAT sin abrir puertos, y también lo que define la latencia que vas a sentir cuando estás en una red hostil.

Por defecto, Headscale entrega a los clientes el mapa de DERP público de Tailscale. Puedes correr tu propio DERP embebido en el mismo servidor de control, y conviene hacerlo si quieres que el tráfico de relay no salga de tu infraestructura:

```yaml
# /etc/headscale/config.yaml
derp:
  server:
    enabled: true
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    # Solo clientes asociados a este servidor pueden usarlo
    verify_clients: true
    # STUN ayuda al NAT traversal
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    # Agregar la región al mapa automáticamente
    automatically_add_embedded_derp_region: true
    # IP pública del servidor, mejora la estabilidad
    ipv4: 203.0.113.10

```

Tres condiciones que la documentación marca como obligatorias si activas el DERP embebido: el `server_url` de Headscale tiene que usar **HTTPS**, porque el DERP necesita TLS; el `stun_listen_addr` tiene que estar definido, porque el DERP escucha STUN por UDP para ayudar al NAT traversal; y la clave privada se genera sola si no existe, pero conviene fijar la ruta para que sobreviva a los reinicios.

Con el DERP embebido activo, el servidor se anuncia a sí mismo como región 999 y los clientes lo prefieren sobre los relays públicos para las conexiones que no pueden ser directas.

## DNS: por qué el nombre del nodo resuelve o no

MagicDNS es lo que te permite llegar a `mi-servidor` en lugar de `100.64.0.2`, y funciona porque Headscale expone su propio servidor de nombres dentro de la malla.

```yaml
# /etc/headscale/config.yaml
dns:
  magic_dns: true
  # Debe ser un FQDN y DISTINTO del dominio del server_url
  base_domain: headscale.tudominio.com
  # Fuerza el DNS de Headscale por sobre el local
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 9.9.9.9
    # Split DNS: un dominio a un resolver distinto
    split:
      "lan.tudominio.com":
        - 192.168.1.1

```

El `base_domain` tiene que ser un FQDN sin punto final y, esto es lo que más se olvida, **distinto del dominio del `server_url`**. Si pones el mismo dominio en los dos, los nombres de nodo colisionan con el nombre del propio servidor de control y la resolución se rompe de formas confusas.

Del lado del cliente, la resolución de MagicDNS depende de que el cliente acepte el DNS. Sin eso, `ping mi-servidor` falla con “name or service not known” mientras que `ping 100.64.0.2` funciona perfecto, que es el síntoma clásico:

```bash
# Verificar que el cliente acepta el DNS del servidor de control
tailscale dns status

# Ver el estado completo de la malla
tailscale status

# Probar la conexión hacia un nodo
tailscale ping --c 3 mi-servidor

```

En Ubuntu 22.04 y posteriores el `systemd-resolved` pelea con Tailscale por la resolución. Si después de `tailscale up` el upstream quedó apuntando al DNS de tu ISP en lugar de `127.0.0.1:53`, el servicio de Tailscale no levantó bien:

```bash
sudo systemctl restart systemd-resolved tailscaled

```

Y si hay un firewall en el nodo, MagicDNS resuelve sobre UDP 53 entre nodos, así que hay que dejarlo pasar dentro del rango de la malla:

```bash
sudo ufw allow in from 100.64.0.0/10 to any port 53

```

## Ciclo de vida: keys, expiración y nodos efímeros

Una malla en producción no es estática. Los nodos se van, se renombran, se vencen. El 0.29.x agrega control sobre el ciclo de vida que antes no existía.

La expiración por defecto de los nodos se controla en la configuración, y el valor por defecto es 0, es decir, sin expiración. Headscale lo dice explícitamente: Tailscale SaaS usa 180 días, y si quieres ese mismo comportamiento, configura un valor positivo:

```yaml
# /etc/headscale/config.yaml
node:
  # 0 = sin expiración por defecto
  expiry: 180d
  ephemeral:
    # Tiempo antes de borrar un nodo efímero inactivo
    inactivity_timeout: 30m

```

Los nodos etiquetados están exentos de la expiración por defecto y nunca expiran solos: la infraestructura se queda mientras exista, que es justo lo que quieres para un servidor de producción. Un cliente OIDC puede sobreescribir el valor con el que viene en el token, pero eso solo aplica si activas `oidc.use_expiry_from_token`.

Para expirar un nodo concreto hay un comando propio, y acepta tres variantes:

```bash
# Expirar ahora mismo
sudo headscale nodes expire --identifier 7

# Expirar en una fecha concreta (RFC3339)
sudo headscale nodes expire --identifier 7 --expiry 2026-12-01T00:00:00Z

# Quitar la expiración de un nodo ya expirado
sudo headscale nodes expire --identifier 7 --disable

```

Los nodos efímeros son la otra mitad: se borran solos después del tiempo de inactividad configurado, y son la opción correcta para contenedores, CI runners y jobs por lote. Un nodo efímero se registra con una pre-auth key marcada como tal:

```bash
# Crear una key para nodos efímeros
sudo headscale preauthkeys create --user 1 --expiration 24h --ephemeral

# Ver los nodos registrados y su expiración
sudo headscale nodes list

```

Y para el día a día hay un conjunto de comandos de gestión que vale la pena tener a mano. Los identificadores numéricos son los que muestra `headscale nodes list` en la primera columna:

```bash
# Renombrar un nodo
sudo headscale nodes rename nuevo-nombre --identifier 3

# Filtrar nodos por usuario
sudo headscale nodes list --user roger

# Ver las rutas aprobadas de un nodo concreto
sudo headscale nodes list-routes --identifier 1

# Eliminar un nodo
sudo headscale nodes delete --identifier 3

```

## Migrar una flota desde Tailscale SaaS

Si vienes de Tailscale SaaS, el orden de las operaciones importa. Primero cambias el `login-server` de los nodos al Headscale nuevo y esos nodos se registran como pendientes de aprobación; después los apruebas en el servidor.

El comando de web auth genera una URL de registro que abres en el navegador para aprobar el nodo a mano. Para flotas, conviene hacerlo con keys desde el principio: se crea una pre-auth key con los tags ya asignados y el nodo se registra sin intervención del administrador.

```bash
# Registrar un nodo de servicio con una key que ya trae el tag
sudo tailscale up --login-server https://vpn.tudominio.com \
  --auth-key tskey-auth-XXXXXXXXXXXXXXXXX

```

El `--auth-key` es el flag vigente del cliente Tailscale; las guías viejas que usan `--authkey` fallan con “flag provided but not defined” en los clientes actuales.

Antes de tocar nada, la actualización del servidor tiene su propio orden y sus propios límites. Headscale aplica una **ruta de upgrade estricta**: hay que pasar de una versión menor a la siguiente sin saltar intermediarios, y siempre eligiendo el último parche disponible. Saltarse una menor está bloqueado, igual que el downgrade. Sube de a una:

```bash
# 0.27.x -> 0.28.x -> 0.29.x, nunca saltando una menor

# Backup de la base antes de cada salto
sudo systemctl stop headscale
sudo cp /var/lib/headscale/db.sqlite /var/lib/headscale/db.sqlite.bak

# Arrancar de nuevo: la migración corre sola
sudo systemctl start headscale

# Verificar el resultado
sudo headscale version
sudo journalctl -u headscale -n 50 --no-pager

```

Con la base en SQLite, que es lo recomendado por la propia documentación de Headscale, la migración tarda un par de segundos en un homelab. El backup previo es tu vuelta atrás si algo sale mal: restaurar el archivo copiado es más rápido que pelear con un downgrade, que está bloqueado.

## Verificación: cinco comprobaciones que valen la pena

Antes de declarar la malla lista, haz estas cinco comprobaciones. En orden, y cada una falla de forma distinta:

```bash
# 1. El servidor de control responde
curl -s -o /dev/null -w '%{http_code}\n' https://vpn.tudominio.com/health

# 2. La política se cargó y no hay errores
sudo journalctl -u headscale -n 30 --no-pager | grep -i policy

# 3. La malla está completa y los tags se ven bien
sudo headscale nodes list

# 4. Las rutas están aprobadas, no solo disponibles
sudo headscale nodes list-routes

# 5. La conexión funciona entre nodos
tailscale ping --c 3 mi-servidor

```

El chequeo 4 es el que más información da. Si una ruta figura en **Available** pero no en **Approved**, el nodo anuncia la ruta y el servidor de control todavía no la autorizó. El síntoma del lado del cliente es que el ping a la IP de la LAN no responde aunque el subnet router esté arriba y con forwarding configurado.

## Cuándo NO usar Headscale para una malla

Con todo lo anterior, conviene ser claro sobre los casos en los que la respuesta correcta es no usarlo:

- **Un solo dispositivo que necesita salir de casa.** Si el problema es “quiero una IP fija en mi portátil”, un exit node comercial o un VPS con WireGuard te resuelve en una hora y te ahorra el servidor de control.
- **Cuando necesitas device posture.** Headscale no soporta `postures` ni `srcPosture`. Si tu política depende de decidir acceso según el estado del dispositivo, eso no se puede portar.
- **Cuando el equipo no va a mantenerlo.** Un servidor de control sin dueño es un firewall abierto dentro de tres meses. Si nadie va a monitorear las políticas, el costo de la segmentación no se paga.
- **IP sets y reglas que dependan de listas dinámicas.** No están soportadas.
- **Cuando la prioridad es el soporte.** Tailscale SaaS te da un panel, historial de cambios y soporte. Acá no hay panel oficial: la GUI de terceros (Headplane) es un proyecto aparte con su propio ciclo de vida.

## Resumen

Lo que hiciste en esta segunda etapa fue pasar de “tengo Headscale corriendo” a “tengo una malla segmentada”. Los tres movimientos que importan:

1. **Identidad por tag, no por dueño.** Los nodos de infraestructura son tagged nodes con `tagOwners` declarados. Los dispositivos personales siguen siendo de un usuario, y esa diferencia es lo que permite escribir reglas que no se rompen cuando alguien se va.
2. **Grants en vez de ACLs.** Headscale 0.29 implementa ambos, pero la documentación recomienda `grants` porque las ACLs están congeladas. Escribe la política en el formato que va a crecer.
3. **Doble opt-in en todo lo que cruza el límite.** Subnet routes y exit nodes se anuncian y se aprueban. Con `autoApprovers` delegas la aprobación, pero la decisión sigue siendo del servidor de control.

Si agregas un segundo router con el mismo prefijo y activas `node.routes.ha`, el failover de la malla deja de depender de que alguien esté mirando. Y si dejas el servidor en la 0.29.7 con el cliente Tailscale al día, la parte que más te va a doler —los parches de SSH— no la vas a sufrir en el cliente.

## Preguntas frecuentes

¿Un nodo puede tener tag y dueño (usuario) al mismo tiempo?

No, son mutuamente excluyentes. Al aplicar un tag, el nodo pierde su dueño de usuario y queda agrupado bajo el usuario especial `tagged-devices`. Por eso todo grant que use un tag necesita que ese tag tenga un owner declarado en `tagOwners`. Para devolverlo a un usuario hay que re-autenticarlo con `tailscale up --advertise-tags= --force-reauth` y aprobarlo con `headscale auth register`.

¿El `tagOwners` que escribiste en la política no toma efecto en los nodos que ya están registrados?

Es la confusión más común con esa sección. `tagOwners` no reasigna tags: define quién tiene derecho a asignarlos, y eso se valida en el momento del registro. Los tags de un nodo que ya está en la malla son los que le pusiste con `headscale nodes tag`. Para cambiarlo, reaplica el tag explícitamente; para darle un owner nuevo, declara ese usuario en `tagOwners` y vuelve a registrar el nodo.

¿Por qué mi subnet router anuncia la ruta pero los otros nodos no llegan a mi LAN?

Lo dice `headscale nodes list-routes`: si la ruta aparece en **Available** pero no en **Approved**, el router la anunció y el servidor de control no la autorizó. Se resuelve con `headscale nodes approve-routes --identifier ID --routes 192.168.1.0/24`. Si ya figura aprobada, el problema está en el router: falta el IP forwarding, el NAT masquerade, o el firewall descarta el tráfico de la interfaz de Tailscale.

¿Un exit node y un subnet router pueden ser el mismo nodo?

Técnicamente sí: un nodo puede anunciar `0.0.0.0/0` y `192.168.1.0/24` al mismo tiempo. En la práctica conviene separarlos. El exit node arrastra todo el tráfico de internet de la malla y el subnet router da acceso a tu LAN: si comparten nodo, una saturación de ancho de banda del exit node afecta también al acceso a la LAN, y el diagnóstico se vuelve más difícil.

¿Puedo saltar de la 0.27 directamente a la 0.29?

No. La ruta de upgrade es estricta: hay que pasar de una versión menor a la siguiente sin saltarse ninguna intermedia, y el downgrade de una menor también está bloqueado. Los parches dentro de la misma menor siempre se permiten, así que lo correcto es 0.27.x → 0.28.x → 0.29.x. Antes de cada salto, frena el servicio y copia la base: si la migración falla, ese backup es tu única vuelta atrás.

## Lee también

- [Headscale: tu propio Tailscale self-hosted en 2026](https://soylinux.com/headscale-tu-propio-tailscale-self-hosted-2026-2/)
- [Configurar Tailscale en Linux: VPN Mesh sin Abrir Puertos para Sysadmins](https://soylinux.com/configurar-tailscale-linux-vpn-mesh-sysadmins/)
- [WireGuard vs Tailscale vs NetBird: comparativa honesta para LATAM 2026](https://soylinux.com/wireguard-vs-tailscale-vs-netbird-comparativa-2026/)
- [Qué es NetBird: la alternativa open source a Tailscale que puedes hostear](https://soylinux.com/netbird-alternativa-open-source-tailscale-2026/)
- [WireGuard Hub-and-Spoke en Linux: cómo unir 3 o más nodos detrás de un servidor central](https://soylinux.com/wireguard-hub-and-spoke-linux-multi-nodo-guia-2026/)
