Sysadmin automation scripts from real tickets (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: you keep getting the same tickets, you keep doing the same steps, and nobody benefits from you clicking through it again.
If you want automation that helps your team, start with your ticket queue—not a new platform.
Use your ticket queue as your backlog
Your ticket system is a list of repeat work. Mine it.
-
Pull the last 2–4 weeks of tickets.
-
Group by request type (not severity).
-
For each group, write down:
- how often it shows up
- how long it takes by hand
Pick the items that happen weekly or daily. They’re the easiest wins.
Also look for requests that block someone from working. Cutting those delays often matters more than the raw minutes you save.
Ticket types worth automating first
In most environments, these land near the top:
- User provisioning and offboarding
- Password resets and account unlocks
- Routine health checks
- Recurring reports (“Can you pull me…?”)
They’re repeatable, they’re easy to standardize, and they directly affect how fast people get back to work.
Active Directory user provisioning (PowerShell)
Account creation is a great target because accuracy matters. The steps are simple, but it’s easy to miss one when you’re 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: don’t email temporary passwords. Use your approved process.
<#
.SYNOPSIS
Bulk create AD users from a CSV, copy groups from a template user, generate temp passwords, and force password change at next logon.
Adds visibility via email summary and a results CSV attachment.
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
#>
param(
[Parameter(Mandatory=$true)]
[string]$CsvPath,
[Parameter(Mandatory=$true)]
[string]$DomainUPNSuffix, # example: yourdomain.com
[Parameter(Mandatory=$true)]
[string]$TargetOU, # example: OU=Users,DC=yourdomain,DC=com
[Parameter(Mandatory=$true)]
[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(
[string]$Subject,
[string]$Body,
[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
}
$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 = ($r.GivenName ?? "").Trim()
$sur = ($r.Surname ?? "").Trim()
$sam = ($r.SamAccountName ?? "").Trim()
if ([string]::IsNullOrWhiteSpace($sam)) {
$results.Add([PSCustomObject]@{
User = "$given $sur".Trim()
Username = ""
UPN = ""
Status = "Error"
Notes = "Missing SamAccountName"
})
continue
}
$displayName = "$given $sur".Trim()
$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 = $r.Department
Title = $r.Title
Path = $TargetOU
AccountPassword = $tempPassword
Enabled = $true
ChangePasswordAtLogon = $true
}
if (-not [string]::IsNullOrWhiteSpace($r.ManagerSam)) {
$mgrDn = (Get-ADUser -Identity $r.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 (PowerShell)
Password resets are common and time-sensitive. They’re 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 systems don’t like that; I’ve seen Horizon need $false.)
<#
.SYNOPSIS
This script resets a user's password and forces a change on login at default.
Adds visibility via email summary on failure and on success if you enable it.
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.
CSV COLUMNS
GivenName,Surname,SamAccountName,Department,Title,ManagerSam,Email
#>
param(
[Parameter(Mandatory=$true)][string]$SamAccountName,
[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([string]$Subject, [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
Set-ADUser -Identity $SamAccountName -ChangePasswordAtLogon $true
$out = [PSCustomObject]@{
Username = $SamAccountName
Action = "Password reset and account unlocked"
ChangeAtNextLogon = $true
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
}
Linux health checks that prevent tickets (Bash)
Some automation doesn’t answer tickets—it reduces how many you get.
Aim for alerts you’ll 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 on 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 over ${THRESHOLD_PERCENT}%:"
disk_out="$(df -Ph | awk -v t="${THRESHOLD_PERCENT}" 'NR==1 {print; next} {gsub("%","",$5); if ($5+0 >= t) print}')"
if [[ -n "${disk_out}" ]]; then
echo "${disk_out}"
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
mail -s "${MAIL_SUBJECT_PREFIX} ${subject_state} ${HOSTNAME_SHORT}" "${MAIL_TO}" < "${report}"
fi
cat "${report}"
rm -f "${report}"
Simple reporting automation (Bash)
A lot of tickets are really “Can you pull a report?”
Here’s 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.
#!/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")
for log in "${LOGS[@]}"; do
if [[ -f "${log}" ]]; then
echo "From ${log}"
grep -E "useradd|new user|password changed|userdel" "${log}" | tail -n 200 || true
echo
fi
done
} > "${report}"
mail -s "${MAIL_SUBJECT_PREFIX} ${DAYS}d $(hostname -s)" "${MAIL_TO}" < "${report}"
cat "${report}"
rm -f "${report}"
How I decide what’s worth automating
I use a quick filter:
- Start here: tasks that show up daily or weekly 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 by hand
If something happens once a year, it usually isn’t worth a script—unless failure would hurt.
Make scripts easy to live with
If a script only works once, it’s not automation. It’s a future ticket.
Keep it simple:
- Do one job per script
- Log actions in plain language
- Use parameters (don’t bake values into the file)
- Write down required permissions and what “good” output looks like
- Fail loudly with clear errors
When automation isn’t the answer
If the process keeps changing, fix the process first. A script won’t save you from a moving target.
And if the task is rare and low risk, doing it by hand may be fine.
A simple start for this week
Pick one ticket type that shows up every week. Script the most repetitive step. Add logging. Write a short README so someone else can run it.
That’s how you build automation that saves time and holds up later.
Closing
Good sysadmin automation comes from your workload. Start with what repeats, write small scripts that are easy to run, and keep the output clear enough to drop into ticket notes.