Altas y bajas en Active Directory con PowerShell: el script que uso en el trabajo
Cómo automatizo las altas y bajas de usuarios en Active Directory con PowerShell: nombre de usuario, contraseña temporal, grupos, carpeta personal y registro.
Dar de alta a alguien en Active Directory a mano son diez clics por cuenta… y en cada uno te puedes equivocar: el usuario en la OU que no toca, un grupo que se te olvida o la carpeta personal sin permisos. Las bajas son peores, porque si se olvidan nadie se queja, y la cuenta de alguien que ya no está sigue activa durante meses.
En el trabajo (sector público) eso lo tenemos resuelto con unos scripts de PowerShell y una ventana sencilla encima. Aquí te cuento cómo están montados para que puedas hacer los tuyos.
Todo lo que sale aquí es genérico: nada de nombres de servidores ni de la organización. Los dominios, rutas y grupos son de ejemplo.
1. Qué hace el alta
El técnico rellena nombre, apellidos y departamento. El resto es opcional: correo, teléfono, descripción y fecha de caducidad (para becarios o contratos temporales). Al pulsar «Dar de alta», el script:
- Calcula el nombre de usuario.
- Genera una contraseña temporal que el usuario tiene que cambiar al entrar por primera vez.
- Crea la cuenta en la OU de su departamento y la mete en sus grupos.
- Crea su carpeta personal con los permisos correctos.
- Avisa a informática por correo y, si se pide, manda la bienvenida al usuario.
- Lo apunta en el registro de auditoría.
- Le pide a GLPI que sincronice los usuarios del directorio en ese momento.
La ventana y la línea de comandos llaman al mismo script. La ventana solo recoge los datos:
.\AD-Alta.ps1 -Nombre "Lucía" -Apellidos "de la Peña Ruiz" -Departamento "Administracion"
2. El nombre de usuario: tildes, eñes y duplicados
Aquí está la mitad de los problemas. La regla que usamos es la inicial del nombre más el primer apellido, en minúsculas y sin caracteres raros. Hay que pensar en tres cosas:
- Tildes y eñes: «Peña» tiene que quedar
pena, nopeanipe?a. - Apellidos compuestos: con «de la Peña», el primer apellido no es «de».
- Duplicados: si ya existe
lpena, la nueva cuenta serálpena2.
function ConvertTo-Ascii([string]$Texto) {
# Separa la letra de su tilde (n + ~) y se queda solo con la letra
$normal = $Texto.Normalize([Text.NormalizationForm]::FormD)
$sb = New-Object Text.StringBuilder
foreach ($c in $normal.ToCharArray()) {
if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($c) -ne 'NonSpacingMark') {
[void]$sb.Append($c)
}
}
$sb.ToString() -replace '[^a-zA-Z0-9]', ''
}
function Get-SamAccountName($Nombre, $Apellidos) {
$particulas = 'de', 'del', 'la', 'las', 'los', 'y'
$ape1 = ($Apellidos -split '\s+' | Where-Object { $_ -notin $particulas })[0]
$base = (ConvertTo-Ascii ($Nombre.Substring(0, 1) + $ape1)).ToLower()
$base = $base.Substring(0, [Math]::Min(18, $base.Length)) # el límite es 20
$sam = $base; $n = 1
while (Get-ADUser -Filter "SamAccountName -eq '$sam'") { $n++; $sam = "$base$n" }
$sam
}
3. Contraseña temporal y creación de la cuenta
La contraseña se genera con un generador criptográfico, no con Get-Random. Además, quito las letras que se confunden al dictarlas (l, I, O, 0):
function New-TempPassword {
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'
$bytes = New-Object byte[] 14
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$pw = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
"$pw#$($bytes[0] % 10)" # siempre lleva un símbolo y un número
}
Y con eso se crea la cuenta. Usar splatting (@params) permite añadir los campos opcionales solo si vienen rellenos:
$sam = Get-SamAccountName $Nombre $Apellidos
$pw = New-TempPassword
$ou = "OU=$Departamento,OU=Usuarios,DC=tu-dominio,DC=local"
$params = @{
Name = "$Nombre $Apellidos"
GivenName = $Nombre
Surname = $Apellidos
SamAccountName = $sam
UserPrincipalName = "$sam@tu-dominio.es"
Path = $ou
Department = $Departamento
AccountPassword = ConvertTo-SecureString $pw -AsPlainText -Force
ChangePasswordAtLogon = $true
Enabled = $true
}
if ($Email) { $params.EmailAddress = $Email }
if ($Expira) { $params.AccountExpirationDate = $Expira }
New-ADUser @params
'GRP-Todos', "GRP-$Departamento" | ForEach-Object {
Add-ADGroupMember -Identity $_ -Members $sam
}
Los grupos siguen siempre el mismo patrón: uno común para todos y uno por departamento. Así el script no necesita una lista aparte: la estructura de OU y grupos ya hace de configuración. Si mañana se crea un departamento nuevo, basta con crear su OU y su grupo.
4. La carpeta personal (y el fallo de la replicación)
$ruta = "\\servidor-ficheros\USUARIOS$\$sam"
New-Item -ItemType Directory -Path $ruta | Out-Null
$sid = (Get-ADUser $sam).SID
$acl = Get-Acl $ruta
$regla = New-Object Security.AccessControl.FileSystemAccessRule(
$sid, 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($regla)
Set-Acl -Path $ruta -AclObject $acl
Fíjate en que la regla usa el SID, no TU-DOMINIO\usuario. Con varios controladores de dominio, la cuenta recién creada puede no haber llegado aún al controlador que consulta el servidor de ficheros. Si usas el nombre, verás el error «no se pudieron traducir algunas o todas las referencias de identidad», pero solo a veces, que es lo peor. Con el SID no hay nada que traducir.
5. Registro de auditoría
Cada alta, baja o borrado añade una línea a un CSV: cuándo, quién, desde qué equipo, qué acción y sobre qué usuario.
function Write-Auditoria($Accion, $Usuario, $Detalle) {
[pscustomobject]@{
Fecha = Get-Date -Format 's'
Tecnico = $env:USERNAME
Equipo = $env:COMPUTERNAME
Accion = $Accion
Usuario = $Usuario
Detalle = $Detalle
} | Export-Csv -Path $LogAuditoria -Append -NoTypeInformation -Encoding UTF8
}
Write-Auditoria 'Alta' $sam "OU=$Departamento; grupos=GRP-Todos,GRP-$Departamento"
Una regla que conviene grabarse: en el registro va que se generó una contraseña, nunca cuál es. El registro se guarda años y lo pueden leer más personas. Si alguien necesita la contraseña temporal, se le entrega por el canal de entrega y en ningún otro sitio.
Si usas GLPI con usuarios del directorio, al terminar puedes lanzar la sincronización por SSH, así la persona ya existe en GLPI para abrirle tickets o asignarle equipos:
ssh admin@glpi.tu-dominio.local "cd /var/www/glpi && sudo -u www-data php bin/console ldap:synchronize_users"
Si te interesa la parte de GLPI, conté cómo lo enlazamos con Wazuh en Wazuh + GLPI: tickets solos y falsas alarmas.
6. Las bajas: deshabilitar, apartar y borrar más tarde
Una baja no borra nada en el momento. Se deshabilita la cuenta, se apuntan sus grupos (por si vuelve o hay que revisar qué tenía) y se mueve a una OU de bajas:
$u = Get-ADUser $sam -Properties MemberOf
Disable-ADAccount -Identity $u
Write-Auditoria 'Baja' $sam ("grupos=" + (($u.MemberOf | ForEach-Object { ($_ -split ',')[0] -replace 'CN=' }) -join ','))
$u.MemberOf | ForEach-Object { Remove-ADGroupMember -Identity $_ -Members $u -Confirm:$false }
Set-ADUser $u -Description "Baja $(Get-Date -Format yyyy-MM-dd)"
Move-ADObject -Identity $u -TargetPath 'OU=Bajas,DC=tu-dominio,DC=local'
Una vez al mes, una tarea programada revisa esa OU y borra las cuentas que llevan más de 180 días de baja. La fecha se lee de la descripción y no de whenChanged, que cambia con cualquier modificación de la cuenta:
$limite = (Get-Date).AddDays(-180)
Get-ADUser -SearchBase 'OU=Bajas,DC=tu-dominio,DC=local' -Filter * -Properties Description |
Where-Object { $_.Description -match '^Baja (\d{4}-\d{2}-\d{2})' -and [datetime]$Matches[1] -lt $limite } |
ForEach-Object {
Write-Auditoria 'Borrado' $_.SamAccountName 'Retencion 180 dias'
Remove-ADUser -Identity $_ -Confirm:$false -WhatIf # quita -WhatIf cuando te fíes
}
7. El informe semanal
Otra tarea programada manda cada lunes un resumen a informática. Lleva las altas y bajas de la semana (salen del CSV de auditoría) y dos listas que evitan sustos:
# Cuentas que caducan en los próximos 30 días (becarios, contratos)
Search-ADAccount -AccountExpiring -UsersOnly -TimeSpan 30.00:00:00
# Cuentas activas que llevan 90 días sin iniciar sesión
Search-ADAccount -AccountInactive -UsersOnly -TimeSpan 90.00:00:00 |
Where-Object Enabled
La segunda lista es la que caza las bajas que nadie comunicó, que en cualquier organización las hay.
8. Detalles de PowerShell 5.1
Los scripts tienen que funcionar en el PowerShell que trae Windows (5.1). Ahí hay una trampa: si el .ps1 está en UTF-8 sin BOM, PowerShell 5.1 lo lee como ANSI y las tildes del código salen rotas. Por eso nuestros scripts están en ASCII puro: sin tildes ni en los comentarios ni en los mensajes. La otra opción es guardarlos en UTF-8 con BOM.
Un instalador pequeño pone en el escritorio del técnico un acceso directo que abre la ventana:
$ws = New-Object -ComObject WScript.Shell
$lnk = $ws.CreateShortcut("$env:USERPROFILE\Desktop\Gestion AD.lnk")
$lnk.TargetPath = 'powershell.exe'
$lnk.Arguments = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Scripts\AD\AD-Gui.ps1"'
$lnk.Save()
Lo que no hay que hacer
- Guardar contraseñas en el código. Ni la de la cuenta que manda los correos ni ninguna otra. Usa el módulo
Microsoft.PowerShell.SecretManagement, o como mínimoExport-Clixml, que cifra con DPAPI para ese usuario en ese equipo. - Escribir la contraseña temporal en el registro, en un ticket o en el chat del equipo.
- Borrar cuentas en el momento de la baja. Deshabilitar es reversible; borrar no.
- Dar por buenas las notificaciones por correo sin probarlas. Haz un alta de prueba cada vez que cambies el servidor de correo. El día que falte un aviso será justo el de una baja.
- Usar
Send-MailMessagepara algo nuevo. Microsoft lo marca como obsoleto. Si lo usas, que sea contra un relé interno y no contra un correo personal.
Resumen
- Un solo script para la ventana y para la consola: nombre de usuario sin tildes ni duplicados, contraseña temporal criptográfica y cuenta creada con
@params. - OU y grupos con un patrón fijo, para que la estructura del directorio haga de configuración.
- Permisos de la carpeta personal por SID, para no depender de la replicación.
- Registro de auditoría en CSV, sin contraseñas.
- Bajas que deshabilitan y apartan, un borrado a los 180 días y un informe semanal que caza las cuentas olvidadas.
Automatizar las altas ahorra tiempo, pero lo que de verdad cambia las cosas es automatizar las bajas: es la parte que a mano siempre se queda a medias.
¿Te has quedado con la idea?
Tres preguntas rápidas. Cada acierto suma 10 XP.