Guides+50 XP

Basic GPOs for a small organisation: how I'd set it up with two policies and PowerShell

How I'd set up GPOs for a small organisation: network drives, printers, agent installs and adding computers, with two policies and PowerShell.

IvánPublished on 8 minLeer en español

When an organisation grows from ten to a few dozen computers, setting up each desk by hand stops being an option: one is missing a network drive, another doesn’t have the printer and on the third nobody installed the inventory agent. If I had to build it from scratch, I wouldn’t create twenty policies: I’d go with two GPOs and a couple of PowerShell scripts anyone on the team can read. Here’s how, with the code and the usual traps.

This is a generic proposal: the domain, paths, groups and names are examples. Adapt them to your network.

1. The map: two GPOs and an OU structure built to grow

GPO What it does Type
GPO-Users-Base Network drives, printers and desktop settings Logon script (user)
GPO-Agents Installs the security and inventory agents Startup script (computer)

To start with, I’d link both at the domain root so they reach everyone. Whatever depends on the person (which shared folder they see, for example) I’d decide inside the script using security groups, not with one GPO per department.

What I would do from day one is an OU structure ready to get finer later:

your-domain.local
├── Servers
├── Leavers
├── Departments          (users: one OU per department)
│   ├── Admin
│   └── Technical
└── Computers-OU         (mirrors the departments)
    ├── Admin
    ├── Technical
    └── Shared           (meeting rooms, reception...)

Keeping users and computers in separate OUs is what you’ll thank yourself for later: the day you want a computer policy for just one department, you link it to its OU and you’re done, no WMI filters. For groups I’d use the same pattern I described in onboarding and offboarding in Active Directory with PowerShell: a GRP-All and a GRP-<Department>.

2. Network drives with a logon script

The textbook way is Group Policy Preferences (GPP) with item-level targeting. It works well, but with lots of drives it turns into a pile of clicks hidden in the console. I prefer a PowerShell script stored in NETLOGON and assigned under:

User Configuration > Policies > Windows Settings > Scripts (Logon/Logoff) > Logon, PowerShell Scripts tab.

A typical layout:

Letter Target For whom
U: \\srv-files\Personal$\<user> Everyone (if their folder exists)
S: \\srv-files\Common Everyone
T: Department folder Depends on their group

The key is that the group → letter → path table sits at the very top. Changing a target means editing one line, not touching the logic:

$Map = @(
    @{ Group = 'GRP-All';       Letter = 'S'; Path = '\\srv-files\Common' }
    @{ Group = 'GRP-Technical'; Letter = 'T'; Path = '\\srv-files\Technical' }
    @{ Group = 'GRP-Admin';     Letter = 'T'; Path = '\\srv-files\Admin' }
)
$FailLog = "$env:TEMP\network-drives.log"

# User's groups, read from the token (no AD module needed on the desktop)
$myGroups = [Security.Principal.WindowsIdentity]::GetCurrent().Groups | ForEach-Object {
    try { $_.Translate([Security.Principal.NTAccount]).Value -replace '^.*\\' } catch {}
}

function Add-Drive($Letter, $Path) {
    if (Get-PSDrive -Name $Letter -ErrorAction SilentlyContinue) { return }   # already there
    try {
        New-PSDrive -Name $Letter -PSProvider FileSystem -Root $Path -Persist -Scope Global -ErrorAction Stop | Out-Null
    } catch {
        net use "${Letter}:" $Path /persistent:yes | Out-Null
        if ($LASTEXITCODE -ne 0) { "$(Get-Date -Format s) ${Letter}: -> $Path failed" | Add-Content $FailLog }
    }
}

$homeDir = "\\srv-files\Personal$\$env:USERNAME"
if (Test-Path $homeDir) { Add-Drive 'U' $homeDir }

foreach ($m in $Map) {
    if ($myGroups -contains $m.Group) { Add-Drive $m.Letter $m.Path }
}

Four details that matter:

  • Check before acting. The script runs at every sign-in; if the letter already exists, it does nothing. You can run it a hundred times without breaking anything.
  • Groups come from the token, not from querying AD. It’s faster and doesn’t need RSAT on the desktop. The trade-off: if you add someone to a group, they have to sign out for it to take effect.
  • There’s a plan B. If New-PSDrive -Persist fails, net use almost always works.
  • Failures get logged. A logon script that fails silently only gets noticed when someone calls saying their U: is gone.

3. Desktop settings without opening the console

The same GPO can carry a few settings applied from PowerShell with Set-GPRegistryValue. That way they live in a script you can repeat in another domain, or use to rebuild the GPO if someone deletes it:

$gpo = 'GPO-Users-Base'

# Lock screen after 5 minutes, password required
$k = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
Set-GPRegistryValue -Name $gpo -Key $k -ValueName ScreenSaveActive    -Type String -Value '1'
Set-GPRegistryValue -Name $gpo -Key $k -ValueName ScreenSaverIsSecure -Type String -Value '1'
Set-GPRegistryValue -Name $gpo -Key $k -ValueName ScreenSaveTimeOut   -Type String -Value '300'

# Autorun off on every drive
Set-GPRegistryValue -Name $gpo -Key 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' `
    -ValueName NoDriveTypeAutoRun -Type DWord -Value 255

A common mix-up: looking for a -Context "Computer" parameter to say whether the setting is for the computer or the user. It doesn’t exist. The hive decides: HKLM is computer configuration and HKCU is user configuration.

4. Printers: in the script too

Printers shared on a print server are added in the same script, one line each:

Add-Printer -ConnectionName '\\srv-print\MFP-Floor1'
Add-Printer -ConnectionName '\\srv-print\MFP-Floor2'

If there are only a couple of multifunction printers for the whole office, there’s no point filtering: let everyone see them. The desktop pulls the driver from the server when it connects. With more printers per floor or per site, that’s when it pays to add them to the group table from step 2 or use the Print Management console.

What causes the most trouble with printers isn’t the GPO but what gets set up by hand on desktops. A printer added with saved credentials for an account that’s no longer active keeps retrying, and every retry is a failed sign-in. To a SIEM that looks a lot like a brute-force attack, and if it’s wired into your ticketing system, a flood is guaranteed. Muting the rule doesn’t fix it: you have to go to the computer and remove that printer. I covered taming this kind of alert in Wazuh + GLPI: automatic tickets and false alarms.

5. Software: agents with a computer startup script

For small things that have to be on every computer, like the Wazuh and GLPI agents, I wouldn’t use classic GPO “Software Installation” (assigning or publishing an MSI). It tells you very little when it fails and doesn’t make passing parameters easy. I prefer a startup script that runs as SYSTEM when the computer boots:

Computer Configuration > Policies > Windows Settings > Scripts (Startup/Shutdown) > Startup

  • Program: powershell.exe
  • Parameters: -ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File \\your-domain.local\NETLOGON\Deploy\Deploy-Agents.ps1

This is done by hand in the console (GPMC): Set-GPRegistryValue can’t assign startup scripts.

The script only installs what’s missing and leaves a log on each computer:

$Log = 'C:\Windows\Temp\agent-deploy.log'
function Write-Log($m) { "$(Get-Date -Format s) $m" | Add-Content $Log }

function Install-Msi($Msi, $Properties, $Name) {
    for ($i = 1; $i -le 5; $i++) {
        $arguments = "/i `"$Msi`" /qn /l*v `"C:\Windows\Temp\$Name-msi.log`" $Properties"
        $p = Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
        if ($p.ExitCode -in 0, 3010) { Write-Log "$Name installed"; return }
        if ($p.ExitCode -eq 1618) { Write-Log "${Name}: another install running, retry $i"; Start-Sleep 60; continue }
        Write-Log "${Name}: error $($p.ExitCode)"; return
    }
}

$source = '\\your-domain.local\NETLOGON\Deploy'

$svc = Get-Service WazuhSvc -ErrorAction SilentlyContinue
if (-not $svc) {
    Install-Msi "$source\wazuh-agent.msi" 'WAZUH_MANAGER="wazuh.your-domain.local" WAZUH_AGENT_GROUP="desktops"' 'Wazuh'
} elseif ($svc.Status -ne 'Running') {
    Start-Service WazuhSvc; Write-Log 'Wazuh was stopped: started'
}

if (-not (Get-Service glpi-agent -ErrorAction SilentlyContinue)) {
    Install-Msi "$source\glpi-agent.msi" 'SERVER="https://glpi.your-domain.local/" RUNNOW=1' 'GLPI'
}

The retry loop is there because of error 1618 (ERROR_INSTALL_ALREADY_RUNNING), the most typical one in this kind of rollout: on a freshly installed computer there’s often another MSI install going on right after boot (updates, Windows itself…). Waiting a minute and retrying sorts it out.

Checking whether a program is installed with Win32_Product is tempting, but that class triggers a consistency check of every MSI on the machine each time you query it. Looking at the service or the registry (HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall) is much cleaner.

If the script never runs on some computers, it’s usually because the network isn’t ready at boot. That’s what Computer Configuration > Policies > Administrative Templates > System > Logon > Always wait for the network at computer startup and logon is for.

6. Adding a computer in a single command

Without an imaging server, the usual routine is to join the computer from sysdm.cpl and then move it to its OU. Add-Computer does it all at once: renames it, joins the domain and drops the computer in its OU:

Add-Computer -DomainName your-domain.local -NewName EQ-1042 `
    -OUPath 'OU=Technical,OU=Computers-OU,DC=your-domain,DC=local' -Credential YOUR-DOMAIN\join-admin -Restart

That way the computer never lands in the Computers container, where no OU policy reaches it. If one slips in there, move it with:

Move-ADObject -Identity 'CN=EQ-1042,CN=Computers,DC=your-domain,DC=local' `
    -TargetPath 'OU=Technical,OU=Computers-OU,DC=your-domain,DC=local'

For names I’d use a fixed format like EQ-<number>, with nothing tied to who uses the computer: the person leaves, the computer stays.

7. Checking everything applies

gpupdate /force     # then sign out (user) or restart (computer)
gpresult /r         # which GPOs applied and which were filtered out
net use             # mapped drives
Get-Printer         # the user's printers
Get-Service WazuhSvc, glpi-agent
Get-Content C:\Windows\Temp\agent-deploy.log -Tail 20

Watch out with gpupdate: it refreshes the policy, but a logon script only runs at sign-in, and a startup script only at boot. And if it says the command doesn’t exist, look closely at what you typed: gpudate has cost me more time than I’d like to admit.

What not to do

  • Write scripts with accented characters if they’ll run on PowerShell 5.1. A UTF-8 .ps1 without BOM is read as ANSI and you get nonsensical syntax errors. Pure ASCII, or UTF-8 with BOM.
  • Use AD filters with {} braces in PowerShell 5.1. They break as soon as you use variable properties ($u.Name); as a string (-Filter "Name -eq '$x'") they work fine.
  • Publish shares under an alias without preparing the server. SMB may reject the connection because of strict name checking, especially when a script running on that same server uses the UNC path. Register the alias with netdom computername <server> /add:<alias> or, as a quick fix, set DisableStrictNameChecking = 1 under HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters and restart the LanmanServer service.
  • Leave computers in Computers. That container isn’t an OU and you can’t link GPOs to it.
  • Name computers after the person using them.

Summary

  1. Two GPOs linked at the root: a user one (drives, printers, settings) and a computer one (agents).
  2. PowerShell scripts with a group table at the top, idempotent, with a plan B and a failure log.
  3. Registry settings with Set-GPRegistryValue; startup scripts by hand in the console.
  4. Agents installed as SYSTEM at boot, with a retry for error 1618 and a log per computer.
  5. Users and computers in separate OUs from day one, and computers added with Add-Computer.

You don’t need a GPO for everything. With a few well-thought-out policies and scripts you can read, anyone on the team knows what each desktop gets and why.

Mini quiz

Did it stick?

Three quick questions. Each right answer is worth 10 XP.

  1. You changed a computer startup script and ran gpupdate /force. Why don't you see any change yet?
  2. msiexec returns error 1618 when installing an agent on a freshly booted computer. What does it mean?
  3. Why does the network drive script check first whether the letter already exists?
  • #gpo
  • #active-directory
  • #powershell
  • #windows-server
  • #sysadmin
Esc