Qué vas a conseguir
Un script que crea en Active Directory los usuarios de un CSV (cada uno en su OU, con sus grupos y una contraseña temporal que nadie ve por correo), que se puede lanzar dos veces sin crear duplicados y que deja un registro de lo que hizo. Y su pareja para las bajas: primero un plan que alguien revisa, después la ejecución, y siempre con los grupos exportados para poder deshacer.
Antes de empezar
- El módulo ActiveDirectory (RSAT en tu equipo o en un servidor de gestión) y una cuenta con permisos delegados para crear y mover usuarios en las OU de destino, no Domain Admin.
- Las OU y los grupos ya creados, y la convención de nombres de la casa.
- Si hay Azure AD Connect o Cloud Sync, saber qué OU están en el ámbito de sincronización: lo que hagas aquí aparece en Entra ID en el siguiente ciclo.
1. El CSV
Una fila por persona. Recursos Humanos o el responsable lo rellena; tú lo revisas.
Nombre;Apellidos;Usuario;Departamento;Puesto;OU;GruposLaura;Prado Ruiz;laura.prado;Finanzas;Analista;OU=Finanzas,OU=Usuarios,DC=contoso,DC=local;GRP-Finanzas|GRP-VPNMarcos;Gil Soto;marcos.gil;Operaciones;Técnico;OU=Operaciones,OU=Usuarios,DC=contoso,DC=local;GRP-Operaciones- Separador
;si el CSV sale de un Excel en español, y guardado como CSV UTF-8 para que las tildes y la ñ lleguen bien. - Los grupos de una persona, separados por
|dentro de la misma columna. - Ninguna contraseña en el CSV. Nunca.
2. El script de altas
[CmdletBinding(SupportsShouldProcess)]param([Parameter(Mandatory)][string]$Csv, [string]$Log = ".\altas-$(Get-Date -Format yyyyMMdd-HHmm).csv")Import-Module ActiveDirectoryfunction New-TempPassword { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!#%+?' $bytes = New-Object byte[] 16 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })}$result = foreach ($r in Import-Csv $Csv -Delimiter ';' -Encoding UTF8) { $sam = $r.Usuario.Trim().ToLower() if (Get-ADUser -Filter "SamAccountName -eq '$sam'") { [pscustomobject]@{ Usuario = $sam; Estado = 'Ya existe'; Detalle = 'No se toca' }; continue } try { $pw = New-TempPassword $p = @{ Name = "$($r.Nombre) $($r.Apellidos)"; GivenName = $r.Nombre; Surname = $r.Apellidos SamAccountName = $sam; UserPrincipalName = "$sam@contoso.com"; DisplayName = "$($r.Nombre) $($r.Apellidos)" Department = $r.Departamento; Title = $r.Puesto; Path = $r.OU; Enabled = $true ChangePasswordAtLogon = $true; AccountPassword = (ConvertTo-SecureString $pw -AsPlainText -Force) } New-ADUser @p -ErrorAction Stop foreach ($g in $r.Grupos -split '\|' | Where-Object { $_ }) { Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop } if (-not $WhatIfPreference) { Write-Host "$sam -> contraseña temporal: $pw" } [pscustomobject]@{ Usuario = $sam; Estado = $(if ($WhatIfPreference) { 'Simulado' } else { 'Creado' }); Detalle = $r.Grupos } } catch { [pscustomobject]@{ Usuario = $sam; Estado = 'Error'; Detalle = $_.Exception.Message } }}$result | Export-Csv $Log -NoTypeInformation -Encoding UTF8 -Delimiter ';'Lo que hace cada pieza:
- Idempotencia: antes de crear, busca el usuario con
Get-ADUser -Filter. Con-Identitydaría error si no existe; con-Filterno devuelve nada. Así el script se puede relanzar tras corregir una fila sin duplicar a nadie. - Contraseña: una distinta por usuario, generada con un generador criptográfico, y con
ChangePasswordAtLogonpara que la persona ponga la suya el primer día. - try/catch por usuario: un fallo en una fila no para el resto.
- El registro: un CSV con usuario, estado y detalle. Se adjunta al ticket, sin contraseñas.
3. Siempre primero con -WhatIf
.\Nuevo-UsuariosAD.ps1 -Csv .\altas-2026-11.csv -WhatIfWhat if: Performing the operation "New" on target "CN=Laura Prado Ruiz,OU=Finanzas,OU=Usuarios,DC=contoso,DC=local".What if: Performing the operation "Set" on target "GRP-Finanzas".What if: Performing the operation "Set" on target "GRP-VPN"..\Nuevo-UsuariosAD.ps1 -Csv .\altas-2026-11.csvComo el script declara SupportsShouldProcess, -WhatIf pasa a New-ADUser y Add-ADGroupMember: enseñan lo que harían sin hacerlo.
4. Entregar la contraseña
La contraseña temporal sale una vez por pantalla y no se guarda. Opciones razonables:
- Dársela al responsable en persona o por teléfono.
- No entregarla: el primer día la persona llama al service desk, se verifica su identidad y se le hace un restablecimiento con cambio obligatorio.
- Con Entra ID, un Temporary Access Pass para registrar MFA.
Nunca en el ticket, en un correo o en el mismo mensaje que el nombre de usuario. Y si usas Read-Host -AsSecureString para teclear una contraseña común para todo el lote, que sea solo para una prueba: una contraseña inicial igual para todos acaba siendo conocida por todos.
5. Las bajas: plan, revisión y ejecución
Para las bajas se cambia el orden: primero se genera un plan que otra persona revisa, y solo después se aplica.
$bajas = Import-Csv .\bajas-2026-11.csv -Delimiter ';'$plan = foreach ($b in $bajas) { Get-ADUser -Filter "SamAccountName -eq '$($b.Usuario)'" -Properties MemberOf, Enabled, Department, LastLogonDate | Select-Object SamAccountName, Name, Department, Enabled, LastLogonDate, @{n='Grupos';e={$_.MemberOf.Count}} }$plan | Export-Csv .\plan-bajas-2026-11.csv -NoTypeInformation -Encoding UTF8 -Delimiter ';'El revisor comprueba que son las personas correctas (no un homónimo) y que nadie sigue trabajando. Con su visto bueno en el ticket:
foreach ($u in $plan) { $user = Get-ADUser $u.SamAccountName -Properties MemberOf $user.MemberOf | Set-Content ".\grupos-$($u.SamAccountName).txt" -Encoding UTF8 Disable-ADAccount -Identity $user Set-ADUser -Identity $user -Description "Baja 2026-11-03 · ticket INC-48213" if ($user.MemberOf) { Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $user.MemberOf -Confirm:$false } Move-ADObject -Identity $user.DistinguishedName -TargetPath "OU=Bajas,DC=contoso,DC=local"}- Exportar los grupos primero: si la baja era un error o la persona vuelve, con ese fichero se le devuelven los permisos en un minuto.
- Deshabilitar, no borrar: el borrado se hace pasado el plazo acordado (30, 60 o 90 días), con la papelera de reciclaje de AD activada.
- Domain Users es el grupo principal y no aparece en
MemberOf, así que no se quita.
Si la OU Bajas está fuera del ámbito de sincronización de Azure AD Connect, mover el usuario allí hace que se borre en Entra ID en el siguiente ciclo (queda 30 días en usuarios eliminados) y con él su buzón y su OneDrive pasan a sus plazos de retención. Decídelo antes, no después.
Siguientes pasos
- La guía de PowerShell de este mismo nivel explica el patrón de objetos,
try/catchy CSV que usa este script. - La guía de Azure AD Connect explica el ámbito de sincronización y qué pasa en Entra ID con cada movimiento de OU.
- Las cuentas que nadie dio de baja se encuentran con una revisión periódica. Es la idea de los repositorios 37-iam-access-review y 03-entra-id-hygiene-audit de mi GitHub.
¿Quieres practicarlo? Resuelve el ticket «El portátil y el móvil de una baja: devolución, borrado y registro» en SysAdmin Desks, gratis y en el navegador.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.
Apúntate y llévate las 33 ideas →