Problema

En entornos mixtos donde servidores Linux (RHEL, CentOS, etc.) se unen a un bosque de Active Directory mediante realmd/SSSD, es frecuente que los escáneres de cumplimiento marquen las cuentas de equipo con el atributo PasswordNeverExpires = TRUE. El síntoma típico es:

  • userAccountControl muestra el bit ADS_UF_DONT_EXPIRE_PASSWD (0x10000) activado.
  • PasswordLastSet indica fechas recientes y los clientes SSSD rotan la contraseña cada 30 días (u otro valor configurado).
  • Los informes de auditoría siguen señalando “PasswordNeverExpires = TRUE”, lo que genera tickets de incumplimiento.

El problema no se limita a una máquina o a una versión concreta de adcli; aparece en despliegues con cientos de nodos y, a menudo, se descubre después de una auditoría externa.

Causa

1. adcli establece el flag por defecto

Hasta la versión 0.9.2 (lanzada en 2021), adcli join incluía el parámetro --dont-expire-password de forma implícita. Cada unión creaba la cuenta de equipo con el bit DONT_EXPIRE_PASSWORD marcado, sin que el operador lo notara.

2. Automatización heredada

Muchas organizaciones mantienen scripts de Ansible, Puppet o Bash que encapsulan la llamada a adcli. Si esos playbooks fueron creados antes de la corrección, siguen pasando el flag, aunque la versión de adcli sea más reciente.

3. Políticas de grupo o delegación manual

En algunos dominios, los administradores de AD aplican una política de “Never expire machine passwords” a OUs específicas para evitar que los controladores de dominio Windows tengan que rotar sus propias cuentas de equipo. Esa política se propaga a los objetos creados después, independientemente del método de unión.

4. Confusión entre expiración de cuenta y expiración de contraseña

El atributo ADS_UF_DONT_EXPIRE_PASSWD es un hint para clientes que no gestionan rotación automática (por ejemplo, Winbind). Cuando SSSD está configurado con ad_maximum_machine_account_password_age, el controlador de dominio no valida la expiración; simplemente permite que el cliente cambie la contraseña cuando lo necesite. Por eso el flag no impide la rotación, pero sí dispara falsos positivos en auditorías que solo inspeccionan userAccountControl.

Solución

La estrategia óptima combina corrección en origen (evitar que el flag se establezca) y remediación masiva (limpiar los objetos existentes). Se pueden seguir estos pasos:

A. Revisar y actualizar la automatización de unión

  1. Buscar el flag en los scripts:

    grep -R -- '--dont-expire-password' /etc/ansible /etc/puppet /opt/scripts
    
  2. Eliminarlo o sustituirlo por --no-dont-expire-password (si la versión lo soporta).
    En versiones de adcli ≥ 0.9.2 el flag es opcional; omitirlo deja el atributo sin marcar.

  3. Probar la unión en un nodo de prueba y confirmar que userAccountControl no contiene 0x10000.

B. Aplicar una política de dominio que anule el flag

En el controlador de dominio, crea o modifica una GPO que establezca “Computer Account Password Never Expires = Disabled” para la OU donde se crean los equipos Linux. La GPO no retroalimenta objetos existentes, pero evita que futuros adcli join los creen con el flag.

C. Limpiar los objetos existentes en bloque

Para entornos con cientos de máquinas, usar PowerShell en un DC o en una máquina con RSAT:

Import-Module ActiveDirectory

# Obtener todas las cuentas de equipo con el flag activo
$computers = Get-ADComputer -Filter {UserAccountControl -band 0x10000} -Properties UserAccountControl

foreach ($c in $computers) {
    # Quitar el bit DONT_EXPIRE_PASSWORD
    $newUac = $c.UserAccountControl -band -0x10001  # elimina 0x10000
    Set-ADComputer -Identity $c.DistinguishedName -Replace @{userAccountControl=$newUac}
}

Este script:

  • Busca solo los equipos afectados, evitando cambios innecesarios.
  • Usa una operación atómica (-Replace) que no altera otros bits.
  • Puede ejecutarse en paralelo con -ThrottleLimit si la OU es muy grande.

D. Forzar una rotación inmediata (opcional)

Después de limpiar el flag, es buena práctica forzar que SSSD regenere la contraseña para asegurarse de que el DC registre la última fecha:

systemctl restart sssd
adcli update --domain example.com

Cuándo aplicar esta solución

  • Síntomas: auditorías que marcan “PasswordNeverExpires = TRUE” en equipos Linux; ldapsearch muestra userAccountControl: 66048 (0x10200) o similar.
  • Entorno: dominio con SSSD configurado (ad_maximum_machine_account_password_age > 0) y máquinas que rotan contraseñas automáticamente.
  • No aplica: si la política de seguridad exige explícitamente que las cuentas de equipo nunca expiren (caso raro, normalmente solo para controladores de dominio Windows).

Código

# 1. Detectar equipos con el flag activo
ldapsearch -LLL -H ldaps://dc.example.com -D "[email protected]" -w "$PASS" \
  "(objectClass=computer)" userAccountControl | grep -B1 "65536"

# 2. Bulk‑clear con PowerShell (ver sección Solución)

Verificación

  1. Post‑limpieza: ejecutar nuevamente la búsqueda LDAP y confirmar que userAccountControl ya no contiene 0x10000.

    ldapsearch -LLL -H ldaps://dc.example.com -D "[email protected]" -w "$PASS" \
      "(userAccountControl:1.2.840.113556.1.4.803:=65536)" dn
    # No debe devolver resultados
    
  2. Comprobar rotación: en un nodo Linux, observar sssctl user-checks <computer$> o revisar /var/lib/sss/db para la última contraseña almacenada; la fecha debe estar dentro del intervalo configurado.

  3. Auditoría: volver a correr el escáner de cumplimiento; el hallazgo debe desaparecer.

Notas adicionales

  • Backup de AD: antes de ejecutar cambios masivos, genera una copia de seguridad del estado de AD (Windows Server Backup o ntdsutil). Un error de cálculo en la máscara de bits puede desactivar atributos críticos.
  • Versiones de adcli: si no puedes actualizar adcli en todos los nodos, considera crear un wrapper script que elimine el flag después del join mediante ldapmodify.
  • Winbind vs SSSD: los clientes que usan Winbind todavía dependen del flag para decidir cuándo cambiar la contraseña. Si tu infraestructura combina ambos, mantén el flag solo en los equipos que usan Winbind.
  • Monitoreo: agrega una alerta en Zabbix/Prometheus que consulte periódicamente userAccountControl y notifique si reaparece el bit. Así evitas que futuros joins vuelvan a introducir el problema.