Enforce Local Account Password Complexity with a Script
Before you run this
This guide turns on "Password must meet complexity requirements" for local accounts on a Windows machine, and shows how to set the related length and age rules in the same pass. It does it by exporting the local security policy to a text template with secedit, editing one line, and importing it back.
A few things to be clear about before you touch anything:
- It needs elevation. Run every command in this guide from an elevated PowerShell session (Run as administrator) on an account with local administrator rights.
seceditwill silently do nothing useful otherwise. - It changes machine security policy, not existing passwords. Enabling complexity does not invalidate current passwords or force anyone out. It is enforced the next time a password is set or changed. So no one is locked out the moment you run it — but the next password reset must satisfy the rules.
- It is reversible, if you keep the backup. The script below exports the current policy to a timestamped backup before changing anything. Restoring that file puts the policy back exactly as it was. Don't skip the backup step.
- Test it on a throwaway machine first. Read the script, then run it on a test VM or a spare workstation and confirm the behaviour before you push it to anything people log into. Never paste-and-run on a live box blind.
- This is for standalone / workgroup machines. On a domain-joined machine, these settings are normally delivered by the Default Domain Policy (or a dedicated password-policy GPO / fine-grained password policy), and a domain GPO will override whatever you set locally at the next policy refresh. If the machine is domain-joined, set this in the domain, not here. See below.
What I'm assuming
- Windows 10/11 or Windows Server 2019/2022, standalone or workgroup (not domain-joined).
- Windows PowerShell 5.1 (the one shipped in the box), running elevated.
secedit.exeis present — it ships with every supported Windows and lives on the defaultPATH.
If your machine is domain-joined, stop here and read Microsoft Learn's documentation on the Default Domain Policy and Password must meet complexity requirements under Security Policy Settings instead.
What "complexity" actually enforces
When PasswordComplexity is enabled, Windows' built-in filter (passfilt.dll) requires that a new password:
- is at least six characters long,
- does not contain the user's account name or significant parts of their full name, and
- contains characters from at least three of these categories: uppercase, lowercase, digits, and non-alphanumeric symbols.
That is the whole rule. It is a yes/no switch — you cannot tune the categories without a custom password filter, which is well outside "standard procedure". The minimum length, maximum age, and history count are separate settings, which is why we set them in the same template.
For the authoritative description, see "Password must meet complexity requirements" on Microsoft Learn (Security policy settings reference).
The script
This exports the current policy, backs it up, flips PasswordComplexity to 1, and applies it. Change $work if you want the files somewhere else.
#Requires -RunAsAdministrator
# Working folder for the exported templates. Change if you like.
$work = 'C:\secpol'
New-Item -ItemType Directory -Path $work -Force | Out-Null
$cfg = Join-Path $work 'secpol.cfg'
$db = Join-Path $work 'secedit.sdb'
$backup = Join-Path $work ("secpol-backup-{0}.cfg" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
# 1. Export the CURRENT local security policy. This file is your rollback point.
secedit /export /cfg $backup
# 2. Work on a copy so the backup stays pristine.
Copy-Item -Path $backup -Destination $cfg -Force
# 3. Read the template (secedit writes it as Unicode / UTF-16).
$content = Get-Content -Path $cfg
# 4. Set the complexity switch. 1 = enabled, 0 = disabled.
$content = $content -replace '^PasswordComplexity\s*=.*', 'PasswordComplexity = 1'
# --- Optional: set the related rules in the same pass. Comment out any you don't want. ---
# Minimum password length (characters):
$content = $content -replace '^MinimumPasswordLength\s*=.*', 'MinimumPasswordLength = 12'
# Maximum password age (days); 0 means "never expire":
$content = $content -replace '^MaximumPasswordAge\s*=.*', 'MaximumPasswordAge = 90'
# Number of old passwords remembered:
$content = $content -replace '^PasswordHistorySize\s*=.*', 'PasswordHistorySize = 5'
# 5. Write it back AS UNICODE — secedit will not read it correctly otherwise.
Set-Content -Path $cfg -Value $content -Encoding Unicode
# 6. Apply only the security-policy area back to the machine.
secedit /configure /db $db /cfg $cfg /areas SECURITYPOLICY
A couple of notes on the non-obvious lines:
-Encoding Unicodematters.secedit /exportwrites UTF-16. If you save the edited file as ASCII/UTF-8, the import can fail or misread values. Keep it Unicode./areas SECURITYPOLICYrestricts the import to account-policy and local-policy settings, so you aren't re-applying user-rights assignments or registry values you never touched.- The
-replacelines rewrite an existing key in place. On a fresh default export all these keys are present, so the replacements will land. If you ever find a key missing from your export, add the line yourself under the[System Access]section rather than letting the replace quietly do nothing. - I set length to 12, age to 90, history to 5 as examples — change those numbers to your standard, or comment the lines out to leave those settings alone.
The minimum length can also be set with net accounts /minpwlen:12, and net accounts is the usual quick tool for the length/age/history knobs. But complexity itself cannot be set with net accounts — that is why we go through secedit.
Verify it worked
net accounts does not report the complexity flag, so don't rely on it for this. Re-export the live policy and read the line back:
$check = Join-Path $work 'verify.cfg'
secedit /export /cfg $check
Get-Content $check | Select-String 'PasswordComplexity|MinimumPasswordLength|MaximumPasswordAge|PasswordHistorySize'
You want to see PasswordComplexity = 1 (plus whatever length/age/history values you set).
For a human-readable cross-check, open secpol.msc → Account Policies → Password Policy and confirm "Password must meet complexity requirements" shows Enabled.
The real test is behavioural: try to set a weak password on a test local account and confirm it's refused.
# On a THROWAWAY test account only — this changes that account's password.
net user testuser weak
With complexity on, a too-simple password is rejected with "The password does not meet the password policy requirements." Use an obviously disposable account name like testuser; don't practise on a real one.
Undo / roll back
Because the script saved a timestamped backup of the original policy before changing anything, rolling back is a one-liner — point secedit /configure at that backup file:
# Replace the filename with your actual backup from C:\secpol
secedit /configure /db (Join-Path 'C:\secpol' 'rollback.sdb') /cfg 'C:\secpol\secpol-backup-YYYYMMDD-HHMMSS.cfg' /areas SECURITYPOLICY
That restores the exact policy state captured at run time. If you only want to turn complexity back off (and keep any other changes), edit the template to PasswordComplexity = 0 and re-run step 6.
Keep those backup .cfg files until you're satisfied the new policy is behaving. They are small, plain text, and they're the difference between a clean rollback and rebuilding policy from memory.
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 a Ping and Port Sweep of a Subnet with PowerShell
This guide builds a small PowerShell script that walks every address in a /24 subnet, sends one ICMP echo (a ping) to each, and — for the hosts that answer — tests a short list of TCP ports and does a reverse-DNS lookup. The output is a CSV inventory: IP, up/down, resolved name, and which ports were open. It is read-only. It does not log into, change, or write anything on the machines it probes.
Rotate Local Admin Passwords Across Machines with PowerShell
This guide gives you a PowerShell script that connects to a list of Windows machines over WinRM and sets a brand-new random password on the built-in local Administrator account of each one , then writes the results to a CSV so you have the new credentials. Its purpose is to kill shared, static, "same password everywhere" local admin accounts.
Automate Expiring AD Password Notifications with PowerShell
This script reads every enabled AD user's password-expiry date, works out who expires within a window you set (say, the next 14 days), and emails each of those users a reminder. It reads Active Directory and sends mail — it does not change a single account, reset a password, or alter a policy. That makes it low-risk, but it can still misfire loudly: point it at the whole domain with a bad window and you can email hundreds of people at once, so treat the first live run as the dangerous part.
Export Installed Software Inventory From Many PCs With PowerShell
This guide builds a script that connects to a list of remote Windows machines over PowerShell remoting (WinRM), reads the "installed programs" data out of each machine's registry, and writes one combined CSV inventory. It is read-only : it queries registry keys and creates a CSV on your admin workstation. It does not install, uninstall, or change anything on the target machines.




