Sysadmin automation tips (PowerShell + Bash examples)
Ticket driven sysadmin automation ideas with short PowerShell and Bash scripts for Active Directory user provisioning, password resets, Linux health checks, and simple reporting.
Sysadmin automation scripts that save you time
Automation usually starts the same way. The same tickets keep landing, you keep running the same checklist, and you still end up doing it manually.
If you want automation that actually helps your team, start with your ticket queue, not a shiny new platform.
Start with the ticket queue
Your ticket system is a list of repeat work. Treat it like one.
- Pull the last two to four weeks of tickets.
- Group by request type, not severity.
- For each group, write down:
- how often it shows up
- how long it takes when you do it by hand
Pick the ones that show up weekly or daily. Those are usually the easiest wins.
Also look for requests that block someone from working. Unblocking people often matters more than shaving a few minutes off your own time.
Good first targets
- User provisioning and offboarding
- Password resets and account unlocks
- Routine health checks
- Recurring reports, the classic “Can you pull me…?”
PowerShell scripts
Active Directory user provisioning
Account creation is a good target because accuracy matters. The steps are simple, but it is easy to miss something when you are juggling tickets.
This example creates users from a CSV, copies group membership from a template user, generates a temporary password, and forces a password change at next sign in.
Security note: do not email temporary passwords. Use whatever your approved process is.
<#
.SYNOPSIS
Bulk create AD users from a CSV, copy groups from a template user, generate temp passwords, and force password change at next logon.
Outputs a results CSV and optionally emails a run summary with that CSV attached.
Deliver the temp password via an approved channel, then have the user change it at first sign in.
REQUIRES
ActiveDirectory module, permissions to create users and add group membership.
CSV COLUMNS
GivenName,Surname,SamAccountName,Department,Title,ManagerSam,Email
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path $_ })]
[string]$CsvPath,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$DomainUPNSuffix, # example: yourdomain.com
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$TargetOU, # example: OU=Users,DC=yourdomain,DC=com
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$TemplateUserSam, # example: jsmith
[switch]$WhatIfOnly,
# Notification settings
[string]$SmtpServer = "smtp.yourdomain.com",
[int]$SmtpPort = 587,
[string]$MailFrom = "automation@yourdomain.com",
[string[]]$MailTo = @("it-team@yourdomain.com"),
[switch]$UseSsl,
[PSCredential]$SmtpCredential,
[switch]$NotifyOnSuccess
)
$ErrorActionPreference = "Stop"
Import-Module ActiveDirectory
Add-Type -AssemblyName System.Web
function New-TempPasswordPlain {
param(
[int]$Length = 16,
[int]$NonAlnum = 3
)
[System.Web.Security.Membership]::GeneratePassword($Length, $NonAlnum)
}
function Send-RunEmail {
param(
[Parameter(Mandatory = $true)][string]$Subject,
[Parameter(Mandatory = $true)][string]$Body,
[Parameter(Mandatory = $true)][string]$AttachmentPath
)
$mailParams = @{
SmtpServer = $SmtpServer
Port = $SmtpPort
From = $MailFrom
To = ($MailTo -join ",")
Subject = $Subject
Body = $Body
Attachments = $AttachmentPath
}
if ($UseSsl) { $mailParams["UseSsl"] = $true }
if ($null -ne $SmtpCredential) { $mailParams["Credential"] = $SmtpCredential }
Send-MailMessage @mailParams
}
function Get-TrimmedValue {
param([object]$Value)
if ($null -eq $Value) { return "" }
return ($Value.ToString()).Trim()
}
$runId = (Get-Date).ToString("yyyyMMdd_HHmmss")
$resultsPath = Join-Path -Path $PSScriptRoot -ChildPath "ad_user_provision_results_$runId.csv"
$rows = Import-Csv -Path $CsvPath
$templateGroups = Get-ADPrincipalGroupMembership -Identity $TemplateUserSam |
Where-Object { $_.Name -ne "Domain Users" }
$results = New-Object System.Collections.Generic.List[object]
foreach ($r in $rows) {
$given = Get-TrimmedValue $r.GivenName
$sur = Get-TrimmedValue $r.Surname
$sam = Get-TrimmedValue $r.SamAccountName
$displayName = ("$given $sur").Trim()
if ([string]::IsNullOrWhiteSpace($sam)) {
$results.Add([PSCustomObject]@{
User = $displayName
Username = ""
UPN = ""
Status = "Error"
Notes = "Missing SamAccountName"
})
continue
}
$upn = "$sam@$DomainUPNSuffix"
try {
$existing = Get-ADUser -Filter "SamAccountName -eq '$sam'" -ErrorAction SilentlyContinue
if ($null -ne $existing) {
$results.Add([PSCustomObject]@{
User = $displayName
Username = $sam
UPN = $upn
Status = "Skipped"
Notes = "User already exists"
})
continue
}
if ($WhatIfOnly) {
$results.Add([PSCustomObject]@{
User = $displayName
Username = $sam
UPN = $upn
Status = "WhatIf"
Notes = "Would create user and copy groups from $TemplateUserSam"
})
continue
}
$tempPasswordPlain = New-TempPasswordPlain
$tempPassword = ConvertTo-SecureString $tempPasswordPlain -AsPlainText -Force
$newUserParams = @{
Name = $displayName
GivenName = $given
Surname = $sur
DisplayName = $displayName
SamAccountName = $sam
UserPrincipalName = $upn
Department = Get-TrimmedValue $r.Department
Title = Get-TrimmedValue $r.Title
Path = $TargetOU
AccountPassword = $tempPassword
Enabled = $true
ChangePasswordAtLogon = $true
}
$email = Get-TrimmedValue $r.Email
if (-not [string]::IsNullOrWhiteSpace($email)) {
$newUserParams["EmailAddress"] = $email
}
$managerSam = Get-TrimmedValue $r.ManagerSam
if (-not [string]::IsNullOrWhiteSpace($managerSam)) {
$mgrDn = (Get-ADUser -Identity $managerSam).DistinguishedName
$newUserParams["Manager"] = $mgrDn
}
New-ADUser @newUserParams
foreach ($g in $templateGroups) {
Add-ADGroupMember -Identity $g.DistinguishedName -Members $sam
}
$results.Add([PSCustomObject]@{
User = $displayName
Username = $sam
UPN = $upn
Status = "Created"
Notes = "Created user and copied groups from template user. Temp password generated, deliver via approved channel."
})
}
catch {
$results.Add([PSCustomObject]@{
User = $displayName
Username = $sam
UPN = $upn
Status = "Error"
Notes = $_.Exception.Message
})
}
}
$results | Export-Csv -NoTypeInformation -Path $resultsPath
$created = ($results | Where-Object Status -eq "Created").Count
$skipped = ($results | Where-Object Status -eq "Skipped").Count
$whatif = ($results | Where-Object Status -eq "WhatIf").Count
$errors = ($results | Where-Object Status -eq "Error").Count
$subjectState = if ($errors -gt 0) { "ERRORS" } else { "OK" }
$subject = "AD provisioning run $subjectState $runId"
$body = @"
AD provisioning run completed.
Created: $created
Skipped: $skipped
WhatIf: $whatif
Errors: $errors
Results CSV attached.
"@
if ($errors -gt 0 -or $NotifyOnSuccess) {
Send-RunEmail -Subject $subject -Body $body -AttachmentPath $resultsPath
}
$results | Format-Table -AutoSize
Write-Host "Results saved to: $resultsPath"
CSV example
GivenName,Surname,SamAccountName,Department,Title,ManagerSam,Email
John,Smith,jsmith,IT,System Administrator,sysadminManager,john.smith@yourdomain.com
Jane,Doe,jdoe,Finance,Analyst,financeManager,jane.doe@yourdomain.com
Password resets and account unlocks
Password resets are common and time sensitive. They are also easy to standardize.
This script resets a password, unlocks the account, and prints clean output you can paste into ticket notes. It forces a password change at next sign in by default. Some environments dislike that, I have seen Horizon prefer $false.
<#
.SYNOPSIS
Reset a domain user's password, unlock the account, and force a password change at next logon by default.
Prints a simple object that is easy to paste into ticket notes.
Deliver the temp password via an approved channel, then have the user change it at first sign in.
REQUIRES
ActiveDirectory module, permissions to reset domain user passwords.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$SamAccountName,
[switch]$DoNotForceChangeAtNextLogon,
[string]$SmtpServer = "smtp.yourdomain.com",
[int]$SmtpPort = 587,
[string]$MailFrom = "automation@yourdomain.com",
[string[]]$MailTo = @("it-team@yourdomain.com"),
[switch]$UseSsl,
[PSCredential]$SmtpCredential,
[switch]$NotifyOnSuccess
)
$ErrorActionPreference = "Stop"
Import-Module ActiveDirectory
Add-Type -AssemblyName System.Web
function Send-Notify {
param(
[Parameter(Mandatory = $true)][string]$Subject,
[Parameter(Mandatory = $true)][string]$Body
)
$mailParams = @{
SmtpServer = $SmtpServer
Port = $SmtpPort
From = $MailFrom
To = ($MailTo -join ",")
Subject = $Subject
Body = $Body
}
if ($UseSsl) { $mailParams["UseSsl"] = $true }
if ($null -ne $SmtpCredential) { $mailParams["Credential"] = $SmtpCredential }
Send-MailMessage @mailParams
}
$runId = (Get-Date).ToString("s")
try {
$newPasswordPlain = [System.Web.Security.Membership]::GeneratePassword(16, 3)
$newPassword = ConvertTo-SecureString $newPasswordPlain -AsPlainText -Force
Set-ADAccountPassword -Identity $SamAccountName -NewPassword $newPassword -Reset
Unlock-ADAccount -Identity $SamAccountName
$changeAtNext = -not $DoNotForceChangeAtNextLogon
Set-ADUser -Identity $SamAccountName -ChangePasswordAtLogon $changeAtNext
$out = [PSCustomObject]@{
Username = $SamAccountName
Action = "Password reset and account unlocked"
ChangeAtNextLogon = $changeAtNext
Timestamp = $runId
NextStep = "Deliver temp password via approved channel"
}
if ($NotifyOnSuccess) {
Send-Notify -Subject "Password reset OK $SamAccountName" -Body ($out | Out-String)
}
$out | Format-List
Write-Host "Temp password generated. Do not paste it into tickets or email. Deliver via approved channel."
}
catch {
$msg = $_.Exception.Message
$body = "Password reset FAILED for $SamAccountName at $runId`nError: $msg"
Send-Notify -Subject "Password reset FAILED $SamAccountName" -Body $body
throw
}
Bash scripts
Linux health checks that prevent tickets
Some automation does not answer tickets, it reduces how many you get.
Aim for alerts you will actually read. If every run emails noise, people ignore it.
This script checks disk usage, a couple of services, and basic connectivity. It emails only when it finds a problem by default. Set ALWAYS_NOTIFY=1 to email every run.
#!/usr/bin/env bash
set -euo pipefail
HOSTNAME_SHORT="$(hostname -s)"
NOW="$(date -Is)"
THRESHOLD_PERCENT="${THRESHOLD_PERCENT:-85}"
ALWAYS_NOTIFY="${ALWAYS_NOTIFY:-0}"
MAIL_TO="${MAIL_TO:-it-team@yourdomain.com}"
MAIL_SUBJECT_PREFIX="${MAIL_SUBJECT_PREFIX:-Health check}"
services=("ssh" "cron")
targets=("1.1.1.1" "8.8.8.8")
report="$(mktemp)"
issues=0
{
echo "Health check for ${HOSTNAME_SHORT}"
echo "Timestamp: ${NOW}"
echo
echo "Disk usage at or above ${THRESHOLD_PERCENT}%:"
disk_rows="$(df -Ph | awk -v t="${THRESHOLD_PERCENT}" 'NR>1 {u=$5; gsub("%","",u); if (u+0 >= t) print}')"
if [[ -n "${disk_rows}" ]]; then
df -Ph | awk 'NR==1 {print}'
echo "${disk_rows}"
issues=1
else
echo " None"
fi
echo
echo "Service status:"
for svc in "${services[@]}"; do
if systemctl is-active --quiet "${svc}"; then
echo " ${svc}: active"
else
echo " ${svc}: NOT active"
issues=1
fi
done
echo
echo "Connectivity:"
for t in "${targets[@]}"; do
if ping -c 1 -W 1 "${t}" >/dev/null 2>&1; then
echo " ${t}: reachable"
else
echo " ${t}: NOT reachable"
issues=1
fi
done
} > "${report}"
subject_state="OK"
if [[ "${issues}" -ne 0 ]]; then
subject_state="ISSUES"
fi
if [[ "${issues}" -ne 0 || "${ALWAYS_NOTIFY}" -eq 1 ]]; then
if command -v mail >/dev/null 2>&1; then
mail -s "${MAIL_SUBJECT_PREFIX} ${subject_state} ${HOSTNAME_SHORT}" "${MAIL_TO}" < "${report}"
else
echo "mail command not found, skipping email" >&2
fi
fi
cat "${report}"
rm -f "${report}"
Simple reporting automation
A lot of tickets are really just, “Can you pull a report?”
Here is a small Linux script that prints recent account related events from common auth logs. Paths vary by distro, so adjust as needed. This version also emails the output when mail is available.
#!/usr/bin/env bash
set -euo pipefail
DAYS="${DAYS:-14}"
SINCE="$(date -d "${DAYS} days ago" +%F)"
MAIL_TO="${MAIL_TO:-it-team@yourdomain.com}"
MAIL_SUBJECT_PREFIX="${MAIL_SUBJECT_PREFIX:-Account events}"
report="$(mktemp)"
{
echo "Account related events in the last ${DAYS} days, since ${SINCE}"
echo
LOGS=("/var/log/auth.log" "/var/log/secure")
found_any=0
for log in "${LOGS[@]}"; do
if [[ -f "${log}" ]]; then
found_any=1
echo "From ${log}"
grep -E "useradd|new user|password changed|userdel" "${log}" | tail -n 200 || true
echo
fi
done
if [[ "${found_any}" -eq 0 ]]; then
echo "No supported auth log found at /var/log/auth.log or /var/log/secure"
fi
} > "${report}"
if command -v mail >/dev/null 2>&1; then
mail -s "${MAIL_SUBJECT_PREFIX} ${DAYS}d $(hostname -s)" "${MAIL_TO}" < "${report}"
else
echo "mail command not found, skipping email" >&2
fi
cat "${report}"
rm -f "${report}"
What I automate and what I skip
I use a quick filter:
- Start here: tasks that show up weekly or daily and take real time
- Next: tasks where mistakes cause outages, security problems, or rework
- Last: rare tasks that are low risk and fast to do manually
If something happens once a year, it usually is not worth a script, unless failure would hurt.
If a script only works once, it is not automation. It is a future ticket.
A few rules that keep this stuff usable:
- Do one job per script
- Log actions in plain language
- Use parameters, do not bake values into the file
- Write down required permissions and what good output looks like
- Fail loudly with clear errors
If the process keeps changing, fix the process first. A script will not save you from a moving target. If the task is rare and low risk, doing it manually may be fine.
Pick one ticket type that shows up every week, script the most repetitive step, add logging, and write documentation so someone else can run it. That is the kind of automation that holds up later.