Problema

En entornos Linux que requieren acceso a recursos protegidos por Azure VPN Point‑to‑Site (P2S) con autenticación Entra ID, la única herramienta oficial es un cliente GUI. Cuando se ejecuta en contenedores, CI/CD runners o servidores sin sesión de escritorio, esa dependencia se vuelve un bloqueo. El síntoma típico es que la conexión nunca se establece; el proceso OpenVPN finaliza después del handshake con un error de autenticación, aunque el usuario ya haya iniciado sesión con az login. La raíz del problema no es la falta de un token, sino que el cliente oficial usa un client‑id propio (41b23e61‑6c1e‑4545‑b367‑cd054e0ed4b4) como audiencia del token. Un token genérico de Azure CLI no coincide y el gateway lo descarta.

Causa

  1. Audiencia del token incorrecta – Azure VPN P2S valida que el aud del JWT sea el ID de la aplicación registrada para el cliente VPN. Cualquier otro aud provoca rechazo inmediato.
  2. Longitud del token – El token de acceso de Entra ID supera los 2 KB, mientras que la implementación estándar de OpenVPN limita la longitud del campo username/password a 128 bytes (USER_PASS_LEN). El truncamiento genera datos corruptos que el gateway interpreta como un intento de autenticación inválido.
  3. Buffers insuficientes – Los buffers de control (TLS_CHANNEL_BUF_SIZE) y de TLS (TLS_MAX_BUF_SIZE) en OpenVPN están dimensionados para credenciales pequeñas. Cuando se envía el token completo, el buffer se desborda y la conexión se reinicia.
  4. Campos OCC y peer‑info – El cliente oficial envía valores OCC (One‑Time‑Challenge) y peer‑info específicos que el gateway verifica. Si estos campos están ausentes o difieren, el túnel se cierra después del handshake.
  5. Dependencia de D‑Bus – La biblioteca propietaria de Microsoft asume la presencia de D‑Bus para resolver certificados. En contenedores sin sesión de escritorio, esa llamada falla y la capa de red nunca se inicializa.

Solución

Una estrategia reutilizable consta de tres capas:

1. Obtención del token con la audiencia correcta

Utiliza el flujo device‑code contra el client‑id del cliente VPN. La respuesta incluye un JWT cuyo aud coincide con la aplicación que el gateway espera.

2. Extender los buffers de OpenVPN

Compila una versión de OpenVPN con los siguientes cambios:

  • USER_PASS_LEN → 4096
  • TLS_CHANNEL_BUF_SIZE → 8192
  • TLS_MAX_BUF_SIZE → 8192

Estos ajustes permiten que el token completo y los campos OCC/peer‑info viajen sin truncamiento.

3. Inyectar OCC y peer‑info idénticos al cliente oficial

Captura una sesión válida del cliente GUI (por ejemplo con strace o LD_PRELOAD sobre SSL_write) y extrae los valores OCC y peer‑info. Luego, añádelos al archivo de configuración de OpenVPN mediante las directivas auth-user-pass-optional y setenv:

setenv OCC "a1b2c3d4e5f6..."
setenv peer-info "AzureVPNClient/2.6.0"

4. Contenedor minimalista

Construye una imagen Docker basada en alpine o ubuntu que:

  • Instale OpenVPN parcheado
  • Incluya curl y jq para el flujo device‑code
  • Configure una interfaz TUN y aplique rutas DNS entregadas por Azure

El contenedor expone un script de entrada que:

  1. Ejecuta el flujo device‑code y guarda el token en /tmp/token.json.
  2. Genera un archivo auth.txt con username vacío y password igual al JWT.
  3. Lanza OpenVPN con --auth-user-pass auth.txt y las variables de entorno OCC/peer‑info.

Al no depender de la biblioteca propietaria, la solución permanece operativa más allá del fin de soporte del cliente oficial.

Cuándo aplicar esta solución

  • Entornos sin GUI: servidores bare‑metal, VMs, contenedores, o runners de CI que necesitan acceso a recursos internos vía Azure VPN P2S.
  • Automatización: scripts que deben establecer la VPN antes de ejecutar pruebas o despliegues y luego desmontarla sin intervención humana.
  • Fin de soporte del cliente oficial: cuando el binario de Microsoft deja de recibir actualizaciones o desaparece del repositorio.

No es necesario cuando:

  • Se dispone de una máquina de escritorio con el cliente oficial y la única necesidad es una conexión interactiva.
  • La política de la organización impide la compilación de binarios personalizados por razones de compliance.

Código

# 1. Iniciar flujo device‑code contra el client‑id del VPN
CLIENT_ID="41b23e61-6c1e-4545-b367-cd054e0ed4b4"
DEVICE_CODE=$(curl -s -X POST \
  -d "client_id=$CLIENT_ID&scope=openid%20profile%20offline_access" \
  https://login.microsoftonline.com/common/oauth2/v2.0/devicecode | jq -r '.device_code, .user_code, .verification_uri')

read -p "Visita ${DEVICE_CODE[2]} e introduce el código ${DEVICE_CODE[1]}. Pulsa ENTER cuando lo hayas hecho..."

# 2. Polling para obtener el token
while :; do
  RESPONSE=$(curl -s -X POST \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=$CLIENT_ID&device_code=${DEVICE_CODE[0]}" \
    https://login.microsoftonline.com/common/oauth2/v2.0/token)
  if echo "$RESPONSE" | grep -q access_token; then
    echo "$RESPONSE" > /tmp/token.json
    break
  fi
  sleep 5
done

# 3. Preparar archivo de credenciales para OpenVPN
jq -r '.access_token' /tmp/token.json > /tmp/vpn_token
printf "\n%s\n" "$(cat /tmp/vpn_token)" > /tmp/auth.txt

# 4. Lanzar OpenVPN con parches y variables OCC/peer‑info
export OCC="a1b2c3d4e5f6..."
export peer_info="AzureVPNClient/2.6.0"
openvpn --config /etc/openvpn/azure-p2s.ovpn \
  --auth-user-pass /tmp/auth.txt \
  --verb 3

Verificación

  1. Estado del túnel – Ejecuta ip addr show tun0 y verifica que la interfaz está UP con la dirección IP asignada por Azure.
  2. Rutasip route debe contener entradas hacia los rangos de red internos que el gateway empujó.
  3. Resolución DNSnslookup <nombre‑interno>.cloudapp.azure.com debe devolver la IP interna del recurso.
  4. Renovación de token – Simula la expiración del JWT (por ejemplo, modificando su campo exp) y comprueba que el script vuelve a ejecutar el flujo device‑code sin intervención manual.

Si todos los pasos se cumplen, la VPN está operativa y lista para ser utilizada por procesos automatizados.

Notas adicionales

  • Persistencia del token: el flujo device‑code devuelve también un refresh_token. Guardarlo permite renovar el access_token sin volver a solicitar al usuario que autorice.
  • Seguridad del contenedor: monta el socket de Docker con --cap-add=NET_ADMIN y evita ejecutar como root dentro del contenedor; usa un usuario sin privilegios y solo eleva CAP_NET_ADMIN cuando sea necesario.
  • Actualizaciones de OpenVPN: al actualizar a una nueva versión, revisa que los valores de los macros (USER_PASS_LEN, TLS_CHANNEL_BUF_SIZE) sigan presentes; de lo contrario, reaplica el parche.
  • Depuración TLS: habilita --verb 5 o superior y revisa los logs de OpenSSL (export OPENSSL_DEBUG=1) para confirmar que el ClientHello lleva el SNI correcto (<gateway>.cloudapp.azure.com).
  • Compatibilidad con WSL2: la misma imagen funciona dentro de WSL2 siempre que el kernel exponga un dispositivo TUN (sudo modprobe tun).

Con estos pasos, cualquier entorno Linux sin interfaz gráfica puede establecer una conexión Azure VPN P2S fiable, manteniendo la automatización y evitando la dependencia de componentes propietarios que pronto dejarán de estar disponibles.