Ir al contenido

FreeRADIUS + TOTP + Active Directory: doble factor para VPN empresarial desde cero

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.
Tabla de contenido

El problema
#

Un FortiGate configurado con SSL-VPN autenticando contra Active Directory solo pide usuario y contraseña. Si esas credenciales se filtran —un phishing, un password reutilizado, un volcado de NTDS— cualquiera entra a la red corporativa desde internet. El segundo factor elimina ese vector: aunque el atacante tenga la contraseña, sin el código TOTP del momento no pasa.

La solución que describo aquí conecta tres piezas:

  • FortiGate: termina la VPN y delega la autenticación a RADIUS
  • FreeRADIUS: recibe la petición y la procesa vía PAM
  • PAM: encadena Google Authenticator (TOTP) + Winbind (AD)

El usuario introduce ContraseñaAD + 123456 como un solo string. FreeRADIUS separa los factores internamente.

Arquitectura del stack
#

[Usuario VPN]
     |
     | ContraseñaAD123456 (PAP sobre TLS)
     v
[FortiGate SSL-VPN]
     |
     | RADIUS Access-Request (PAP)
     v
[FreeRADIUS]
     |
     | /etc/pam.d/radiusd
     v
[PAM]
  ├── pam_google_authenticator  ← valida últimos 6 dígitos (OTP)
  └── pam_winbind               ← valida el resto contra AD

El flujo PAP es obligatorio aquí: RADIUS necesita el password en claro para que PAM pueda separarlo y validar cada factor. CHAP o MS-CHAPv2 no funcionan con esta arquitectura.

Instalación
#

En Ubuntu 22.04 / Debian 12:

apt update
apt install freeradius freeradius-config \
            winbind samba libnss-winbind \
            libpam-winbind libpam-google-authenticator

Verificar que FreeRADIUS arranca antes de tocar configuración:

systemctl stop freeradius
freeradius -X   # modo debug, ver output limpio

Unir el servidor Linux a Active Directory
#

Editar /etc/samba/smb.conf. La sección [global] debe quedar así:

[global]
   workgroup = DOMINIO
   realm = DOMINIO.LOCAL
   security = ads
   encrypt passwords = yes

   winbind use default domain = yes
   winbind enum users = yes
   winbind enum groups = yes
   winbind offline logon = no
   winbind separator = +

   idmap config * : backend = tdb
   idmap config * : range = 3000-7999

   idmap config DOMINIO : backend = rid
   idmap config DOMINIO : range = 10000-99999

El bloque idmap config DOMINIO es el que más problemas causa cuando falta (ver sección de errores). Sin él, getpwnam() no resuelve los usuarios del dominio y FreeRADIUS falla con “user not found” aunque el usuario exista en AD.

Unirse al dominio:

net ads join -U Administrador
systemctl enable --now winbind

Verificar que los usuarios del dominio son visibles:

wbinfo -u | head
getent passwd DOMINIO\\usuario.prueba

Si getent devuelve la línea del usuario, la integración AD funciona.

Configuración de PAM
#

Crear o editar /etc/pam.d/radiusd:

auth    required    pam_google_authenticator.so \
        forward_pass \
        secret=/etc/google-authenticator/${USER} \
        user=freerad

auth    required    pam_winbind.so use_first_pass

account required    pam_winbind.so

Tres directivas clave:

forward_pass en pam_google_authenticator: el módulo extrae los últimos 6 caracteres del password recibido, los valida como TOTP, y pasa el resto hacia adelante en la pila PAM. Sin esta directiva, el módulo consumiría el password entero y pam_winbind no tendría nada que validar.

use_first_pass en pam_winbind: fuerza al módulo a usar el password que quedó en la pila (el que pasó forward_pass) en lugar de pedirlo de nuevo. Si se omite, intenta leer desde stdin y cuelga o falla.

user=freerad: pam_google_authenticator lee el fichero de secreto del usuario que hace la petición. El problema es que por defecto intenta leerlo como ese usuario de sistema, y el usuario del dominio no tiene permisos en /etc/google-authenticator/. Fijando user=freerad, el módulo siempre lee como el usuario freerad (proceso de FreeRADIUS), que sí tiene acceso.

Ficheros TOTP por usuario
#

Crear el directorio de secretos:

mkdir -p /etc/google-authenticator
chown freerad:freerad /etc/google-authenticator
chmod 700 /etc/google-authenticator

Para cada usuario del dominio, generar el secreto y dejar el fichero donde PAM lo buscará:

# Ejecutar como el propio usuario, o con google-authenticator y mover el fichero
su -s /bin/bash -c "google-authenticator -t -d -f -r 3 -R 30 -w 3" freerad
# O generarlo manualmente y colocarlo:
mv ~/.google_authenticator /etc/google-authenticator/usuario.dominio
chown freerad:freerad /etc/google-authenticator/usuario.dominio
chmod 600 /etc/google-authenticator/usuario.dominio

Los permisos 0600 son obligatorios. pam_google_authenticator rechaza ficheros con permisos más permisivos como medida de seguridad.

Configuración de FreeRADIUS
#

Declarar el cliente FortiGate
#

En /etc/freeradius/3.0/clients.conf, añadir:

client fortigate-vpn {
    ipaddr      = IP-FORTIGATE
    secret      = secreto-compartido-seguro
    shortname   = fortigate
    nastype     = other
}

Activar autenticación PAP en el virtual server
#

El virtual server por defecto está en /etc/freeradius/3.0/sites-enabled/default. En la sección authorize, añadir antes del bloque pap:

authorize {
    ...
    update control {
        &Auth-Type := PAP
    }
    pap
    ...
}

Esto fuerza a FreeRADIUS a usar PAM/PAP para todos los usuarios. Si hay otros métodos activos (LDAP, SQL), asegurarse de que este bloque tiene prioridad o va en un virtual server dedicado.

Activar el módulo PAM
#

En /etc/freeradius/3.0/mods-enabled/, comprobar que existe el enlace simbólico pam:

ls -la /etc/freeradius/3.0/mods-enabled/pam
# Si no existe:
ln -s /etc/freeradius/3.0/mods-available/pam \
      /etc/freeradius/3.0/mods-enabled/pam

En la sección authenticate del virtual server:

authenticate {
    Auth-Type PAP {
        pam
    }
    ...
}

Configuración del FortiGate
#

En la consola FortiGate:

  1. Servidor RADIUS: User & Authentication → RADIUS Servers → Create New

    • IP/Name: IP-SERVIDOR-RADIUS
    • Secret: el mismo secreto-compartido-seguro
    • Authentication method: PAP
  2. Grupo de usuarios: crear un grupo que use el servidor RADIUS como fuente.

  3. SSL-VPN: en la política SSL-VPN, asociar el grupo RADIUS.

PAP debe estar explícito en FortiGate. Si queda en “auto”, FortiGate puede intentar MS-CHAP y la autenticación falla silenciosamente.

Pruebas
#

Antes de tocar el FortiGate, probar localmente con radtest:

# Iniciar FreeRADIUS en modo debug
systemctl stop freeradius
freeradius -X &

# Probar: password = ContraseñaAD + OTP del momento
radtest nombre.usuario "MiPassword123456" 127.0.0.1 0 secreto-local

# Respuesta esperada:
# Received Access-Accept Id 0 from 127.0.0.1:1812

Si aparece Access-Reject, el modo debug de FreeRADIUS muestra exactamente en qué paso PAM falló. Leer el output completo antes de buscar en otra parte.

Para probar winbind por separado:

wbinfo -a 'DOMINIO\usuario.prueba%ContraseñaAD'
# Respuesta esperada: plaintext password authentication succeeded

Errores frecuentes
#

Failed to change user id to "nombre.usuario"
#

FreeRADIUS intenta ejecutar pam_google_authenticator como el usuario del dominio, que no existe como usuario Unix local.

Solución: añadir user=freerad en la línea de pam_google_authenticator en /etc/pam.d/radiusd.


user("nombre.usuario") not found
#

Winbind no resuelve el usuario del dominio. Casi siempre falta la configuración de idmap para el dominio específico en smb.conf.

Solución: añadir en [global]:

idmap config DOMINIO : backend = rid
idmap config DOMINIO : range = 10000-99999

Reiniciar winbind después: systemctl restart winbind. Confirmar con getent passwd DOMINIO\\usuario.


Secret file permissions (0644) are more permissive than 0600
#

El fichero de secreto TOTP tiene permisos demasiado abiertos.

Solución:

chmod 600 /etc/google-authenticator/nombre.usuario

Failed to create tempfile: Permission denied
#

pam_google_authenticator intenta escribir un fichero temporal en el directorio de secretos (para el rate limiting y los códigos de un solo uso) y no tiene permiso.

Solución: el directorio /etc/google-authenticator/ debe ser propiedad de freerad:

chown freerad:freerad /etc/google-authenticator
chmod 700 /etc/google-authenticator

Auth-Type already set o autenticación en bucle
#

Hay un módulo LDAP o SQL activo que también intenta establecer Auth-Type. El bloque update control { &Auth-Type := PAP } usa := (asignación forzada) precisamente para sobreescribir lo que otros módulos hayan puesto. Verificar que no hay otro bloque estableciendo Auth-Type después.

Conclusión
#

El stack FreeRADIUS + PAM + Google Authenticator + Winbind cubre el requisito de MFA en VPN sin necesidad de licencias adicionales ni appliances dedicados. El coste operativo real está en la gestión de los ficheros TOTP por usuario: cada incorporación requiere generar el secreto, compartir el QR con el usuario y asegurarse de que el fichero tiene los permisos correctos.

Para entornos con más de 20-30 usuarios, tiene sentido automatizar ese proceso de onboarding con un script que genere el fichero, lo ubique en /etc/google-authenticator/ y envíe el QR por correo cifrado. La base técnica que describe este artículo es la misma independientemente de la escala.

El punto más frágil de la arquitectura es el fichero de secreto en disco: si alguien con acceso root al servidor RADIUS extrae esos ficheros, puede clonar los tokens TOTP. Limitar el acceso SSH al servidor, activar auditoría y rotar los secretos periódicamente son las medidas de higiene mínimas para mantener el segundo factor real.