
Automate a Windows Server Hardening Baseline Check with PowerShell
Before you run this
This script is a read-only audit. It reports PASS/FAIL for a small set of well-known hardening settings — SMBv1 status, firewall profiles, Defender real-time protection, RDP Network Level Authentication, and the local Guest account — and writes the results to the screen and a CSV. It changes nothing. There is nothing to roll back, which is exactly why an audit script is the safe place to start before you touch any actual configuration.
It needs an elevated session. Run it in a PowerShell console started with "Run as Administrator." Several of the checks read machine-scope state (the RDP-Tcp registry key, Defender status, SMB server configuration) that a standard user cannot see reliably, and you'll get misleading FAILs without elevation. The #Requires -RunAsAdministrator line at the top will stop the script rather than let it run half-blind.
Even though it only reads, read the script before you run it and try it on a non-production server or a test VM first. You want to confirm the output format and that each check succeeds in your environment before you trust it as a baseline. A check that silently errors and reports FAIL is worse than no check.
One honest limitation: this validates a handful of settings I picked because they're common, high-value, and cheap to read. It is not a full baseline. Treat a clean run as "these specific things look right," not "this server is hardened." For an authoritative baseline, use the Microsoft Security Compliance Toolkit's security baselines or the CIS Benchmark for your OS version — both are linked at the end.
What I assume
- Windows Server 2022, domain-joined or standalone member server (not a domain controller —
Get-LocalUserdoesn't apply to DCs). - Windows PowerShell 5.1, the in-box shell. Everything here also runs under PowerShell 7 on the same box.
- Microsoft Defender Antivirus is the installed AV. If you run a third-party product, the Defender check will report FAIL or error; adjust that check or ignore it.
- The
NetSecurity,SmbShare,Defender, andMicrosoft.PowerShell.LocalAccountsmodules are present — they ship with Server 2022 by default.
What the script checks
| Check | What "pass" means |
|---|---|
| SMBv1 server disabled | EnableSMB1Protocol is False |
| Firewall profiles enabled | Domain, Private, and Public profiles are all on |
| Defender real-time protection | Real-time protection is enabled |
| RDP Network Level Authentication | NLA is required for RDP connections |
| Local Guest account disabled | The built-in Guest account is disabled |
Each check is wrapped so that if the underlying query fails, it reports FAIL with the error rather than crashing the whole run.
The script
Save this as Test-HardeningBaseline.ps1. Change the CSV path placeholder to a real folder you can write to.
#Requires -RunAsAdministrator
# Read-only Windows Server 2022 hardening baseline check.
# Reports PASS/FAIL for a small set of common settings. Changes nothing.
$OutputCsv = 'C:\path\to\hardening-check.csv' # <-- replace with a real path
$results = New-Object System.Collections.Generic.List[object]
function Add-Result {
param([string]$Check, [bool]$Pass, [string]$Detail)
$results.Add([pscustomobject]@{
Check = $Check
Status = if ($Pass) { 'PASS' } else { 'FAIL' }
Detail = $Detail
})
}
# --- SMBv1 server protocol ---
try {
$smb1 = (Get-SmbServerConfiguration).EnableSMB1Protocol
Add-Result 'SMBv1 server disabled' (-not $smb1) "EnableSMB1Protocol = $smb1"
} catch {
Add-Result 'SMBv1 server disabled' $false "Query failed: $($_.Exception.Message)"
}
# --- Firewall profiles (one row per profile) ---
try {
foreach ($p in Get-NetFirewallProfile) {
# .Enabled is an enum; compare as string to be safe
$on = ("$($p.Enabled)" -eq 'True')
Add-Result "Firewall profile '$($p.Name)' enabled" $on "Enabled = $($p.Enabled)"
}
} catch {
Add-Result 'Firewall profiles enabled' $false "Query failed: $($_.Exception.Message)"
}
# --- Defender real-time protection ---
try {
$mp = Get-MpComputerStatus -ErrorAction Stop
Add-Result 'Defender real-time protection on' ([bool]$mp.RealTimeProtectionEnabled) `
"RealTimeProtectionEnabled = $($mp.RealTimeProtectionEnabled)"
} catch {
Add-Result 'Defender real-time protection on' $false 'Get-MpComputerStatus unavailable (third-party AV?)'
}
# --- RDP Network Level Authentication ---
# UserAuthentication = 1 means NLA is required.
try {
$key = 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
$nla = (Get-ItemProperty -Path $key -Name UserAuthentication -ErrorAction Stop).UserAuthentication
Add-Result 'RDP NLA required' ($nla -eq 1) "UserAuthentication = $nla"
} catch {
Add-Result 'RDP NLA required' $false "Could not read RDP-Tcp key: $($_.Exception.Message)"
}
# --- Local Guest account disabled ---
try {
$guest = Get-LocalUser -Name 'Guest' -ErrorAction Stop
Add-Result 'Local Guest account disabled' (-not $guest.Enabled) "Enabled = $($guest.Enabled)"
} catch {
Add-Result 'Local Guest account disabled' $true 'No local Guest account found'
}
# --- Output ---
$results | Format-Table -AutoSize
$results | Export-Csv -Path $OutputCsv -NoTypeInformation
$failed = ($results | Where-Object Status -eq 'FAIL').Count
Write-Host ""
Write-Host "Checks failed: $failed of $($results.Count)" -ForegroundColor $(if ($failed) {'Yellow'} else {'Green'})
Write-Host "Report written to: $OutputCsv"
The only line to edit is $OutputCsv. Everything else is generic.
Running it
Open PowerShell as Administrator, then run it. If your execution policy blocks local scripts, run it for this process only rather than weakening the machine policy:
powershell.exe -ExecutionPolicy Bypass -File .\Test-HardeningBaseline.ps1
-ExecutionPolicy Bypass here applies to that single invocation; it does not change the machine's stored policy.
Reading the output and verifying it worked
You'll get a table on screen and a CSV at the path you set. Because this script only reads, "verifying it worked" means confirming the checks ran, not that they changed anything:
- Every row has a
Statusof PASS or FAIL — none are blank or erroring. - The
Detailcolumn shows a real value (e.g.EnableSMB1Protocol = False), not a "Query failed" message. A FAIL with an error string means the check couldn't run, which is different from the setting being wrong — investigate those first. - The CSV exists and matches the on-screen table:
Import-Csv 'C:\path\to\hardening-check.csv' | Format-Table -AutoSize
You can spot-check any single item by hand against the same source the script used, for example:
Get-SmbServerConfiguration | Select-Object EnableSMB1Protocol
Get-NetFirewallProfile | Select-Object Name, Enabled
Undo
There is nothing to undo — the script makes no changes to the system. If you delete the CSV, delete the .ps1, and close the console, no trace of it remains on the server's configuration. That's the point of keeping the audit and the remediation as separate steps.
When a check comes back FAIL and you decide to fix it, do that as its own deliberate change — with a config backup or a system state backup first, and in a maintenance window — not bolted onto this script. Remediation cmdlets like Set-SmbServerConfiguration or Disable-LocalUser do change the system and belong in a reviewed change, not an audit run.
Where to get a real baseline
This script is a quick sanity check, not a substitute for a maintained baseline. For the authoritative settings and the tooling to apply and measure them:
- Microsoft Security Compliance Toolkit and the Windows Server security baselines — search Microsoft Learn for "Security Compliance Toolkit"; it includes the Policy Analyzer and the baseline GPO backups.
- CIS Benchmarks for Windows Server — the Center for Internet Security publishes versioned benchmarks and a scoring tool.
Use those to decide what your baseline should be; use a script like this to spot-check that a given server still matches the pieces you care about most.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Automate DNS Record Audits on Windows DNS with PowerShell
This guide builds a read-only audit of a Microsoft DNS server: it enumerates the zones, exports every resource record to CSV, and produces a short report of records that look stale (dynamic records whose aging timestamp is older than a threshold you set). The audit script itself creates nothing and deletes nothing — its worst case is a CSV file on disk.
Automate a Daily Failed-Logon (4625) Report with PowerShell
This guide builds a scheduled PowerShell job that reads Event ID 4625 (failed logon) from the Windows Security log for the last 24 hours and writes them to a dated HTML report. It is read-only — it queries the event log and creates a report file. It does not change auditing policy, delete events, or touch accounts.
Automate a Network Share Permissions Audit with PowerShell
This guide builds a read-only report . The script enumerates the SMB shares on a Windows file server, then lists two things for each one: the share-level permissions (the "who can connect" layer) and the NTFS permissions on the folder behind it (the "who can touch the files" layer). It writes both to CSV so you can review access in a spreadsheet instead of clicking through the Security tab share by share.
Automate Certificate Expiry Checks on Windows with PowerShell
An expired TLS certificate is the kind of outage that is entirely preventable and still catches everyone. This guide builds a small PowerShell script that reads the certificates in a server's own store, flags any that expire soon, and then schedules it to run daily so you hear about it weeks in advance instead of from a monitoring alert at 2 a.m.




