Scott Coffman Logo

Sysadmin automation scripts from real tickets (PowerShell + Bash examples)

December 21, 2025 9 min read

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.

  1. Pull the last 2–4 weeks of tickets.

  2. Group by request type (not severity).

  3. 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:

  1. User provisioning and offboarding
  2. Password resets and account unlocks
  3. Routine health checks
  4. 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:

  1. Do one job per script
  2. Log actions in plain language
  3. Use parameters (don’t bake values into the file)
  4. Write down required permissions and what “good” output looks like
  5. 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.

Related Posts

About the Author

Scott Coffman is a DevOps and web systems consultant who writes about automation, infrastructure, and building reliable systems.

Learn more at scottcoffman.com .