PowerShell-scripts als scheduled task: doe het veilig
Een PowerShell-script dat je zelf uitvoert, controleer je met je eigen ogen. Een script dat als scheduled task draait, doet zijn werk om drie uur ’s nachts terwijl niemand kijkt. Het draait vaak met meer rechten dan een gewone gebruiker, het heeft soms een wachtwoord nodig om bij een systeem te komen, en het staat gewoon als bestand op een server. Dat zijn precies de eigenschappen waar een aanvaller naar op zoek is.
Geautomatiseerde taken zijn daarmee niet onveilig, maar ze vragen wel om een paar maatregelen die je bij handwerk kunt overslaan. Hieronder de belangrijkste.
Begin bij het account
Voordat je aan scripts en wachtwoorden denkt: onder welk account draait de taak? Dit is de keuze die het meeste bepaalt.
Draai een scheduled task nooit onder een domeinadmin of een persoonlijk account. Gebruik een apart serviceaccount met precies de rechten die de taak nodig heeft, en niets meer. Raakt het script of het account gecompromitteerd, dan blijft de schade beperkt tot wat die taak mocht.
Het mooiste is een group Managed Service Account (gMSA). Active Directory beheert dan zelf het wachtwoord en roteert het automatisch. Je hoeft nergens een wachtwoord op te slaan, en Task Scheduler kan een taak onder een gMSA draaien, ook als er niemand is ingelogd. Kan dat in jouw omgeving, dan los je het wachtwoordprobleem hieronder in één klap op.
Lukt een gMSA niet, dan heb je wel een manier nodig om een wachtwoord veilig te bewaren. Daarover verderop meer.
Code signing: weten dat het script onveranderd is
Waarvoor: een handtekening op een script bewijst twee dingen. Dat het script van jou komt, en dat het sinds het ondertekenen niet is aangepast. Verandert iemand er één teken aan, bijvoorbeeld om er een kwaadaardige regel in te smokkelen, dan is de handtekening ongeldig en weigert het systeem het uit te voeren. Voor een taak die onbeheerd en met rechten draait, is dat een belangrijke beveiliging tegen manipulatie.
Hoe je het toepast: je hebt een code-signing-certificaat nodig. In een bedrijfsomgeving haal je dat bij je interne certificate authority; voor een testomgeving kun je een self-signed certificaat maken. Ondertekenen gaat zo:
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
Set-AuthenticodeSignature -FilePath .\mijn-taak.ps1 -Certificate $cert `
-TimestampServer "http://timestamp.digicert.com"
De TimestampServer is belangrijk: die zet een tijdstempel bij de handtekening, zodat het script geldig blijft nadat het certificaat is verlopen. Zonder tijdstempel zou je elk jaar alles opnieuw moeten ondertekenen.
Daarna dwing je af dat alleen ondertekende scripts draaien, met de execution policy:
Set-ExecutionPolicy AllSigned -Scope LocalMachine
Met AllSigned draait geen enkel script zonder geldige handtekening. In de praktijk roep je het script vanuit Task Scheduler bovendien aan met vaste parameters, zodat de omgeving voorspelbaar is:
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy AllSigned -File "C:\Scripts\mijn-taak.ps1"
Let op: de execution policy is een beschermingslaag, geen onneembare muur. Iemand die al beheerrechten op de machine heeft, kan hem omzeilen. De waarde zit in het tegenhouden van onbedoeld of gemanipuleerd uitgevoerde scripts, niet in het stoppen van een aanvaller die de server al volledig in handen heeft.
Wachtwoordbestanden met afgeschermde ACL’s
Soms ontkom je er niet aan dat een taak een wachtwoord nodig heeft, bijvoorbeeld voor een systeem dat geen gMSA of integrated authentication ondersteunt. Het wachtwoord hardcoded in het script zetten is dan het slechtste wat je kunt doen: iedereen die het script kan lezen, kan het wachtwoord lezen.
Waarvoor: een afgeschermd wachtwoordbestand lost twee problemen tegelijk op. Het wachtwoord staat versleuteld op schijf, en met een strakke ACL bepaal je dat alleen het serviceaccount erbij kan.
Hoe je het toepast: PowerShell kan een wachtwoord versleutelen met DPAPI, de Data Protection API van Windows. Het resultaat kan alleen worden teruggelezen door hetzelfde account op dezelfde machine dat het heeft versleuteld. Dat is precies wat je wilt: kopieert iemand het bestand naar een andere computer of opent een ander account het, dan is het waardeloos.
Je maakt het bestand eenmalig aan, ingelogd als het serviceaccount op de doelserver:
# Eenmalig, als het serviceaccount op de server zelf
Get-Credential | Export-Clixml -Path "C:\Scripts\secrets\taak.cred"
In het script lees je het terug:
$cred = Import-Clixml -Path "C:\Scripts\secrets\taak.cred"
# $cred is een PSCredential die je direct kunt gebruiken, bijv.:
Invoke-Command -ComputerName SQL01 -Credential $cred -ScriptBlock { ... }
Zet daar vervolgens een strakke ACL op, zodat alleen het serviceaccount (en beheerders) het bestand kan lezen:
$acl = Get-Acl "C:\Scripts\secrets\taak.cred"
$acl.SetAccessRuleProtection($true, $false) # overgeërfde rechten eraf
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"DOMEIN\svc-mijntaak", "Read", "Allow")
$acl.SetAccessRule($rule)
Set-Acl "C:\Scripts\secrets\taak.cred" $acl
Twee lagen dus: de inhoud is versleuteld en aan account plus machine gebonden, en de ACL houdt nieuwsgierige ogen bij het bestand weg.
Let wel op de grenzen. DPAPI beschermt niet tegen het serviceaccount zelf: wie code kan draaien áls dat account, kan het wachtwoord teruglezen. Dat is inherent, want de taak moet het immers ook kunnen. Bewaar daarom alleen wat echt nodig is, en geef het account zo min mogelijk rechten.
De modernere route: sinds een paar jaar is er de module Microsoft.PowerShell.SecretManagement met SecretStore. Dat geeft je een echte, versleutelde kluis met een eenduidige manier om secrets op te halen (Get-Secret), los van je scriptcode. Voor nieuwe oplossingen is dat de voorkeur boven losse .cred-bestanden. En nogmaals: kun je een gMSA gebruiken, dan heb je helemaal geen opgeslagen wachtwoord meer nodig.
Nog een paar best practices
Laat de taak merkbaar falen. Zet bovenaan $ErrorActionPreference = 'Stop' en vang fouten op met try/catch. Sluit af met een exit code, zodat Task Scheduler het verschil ziet tussen geslaagd en mislukt:
try {
# ... het werk ...
exit 0
} catch {
Write-Error $_
exit 1
}
Een taak die stilletjes niets doet is gevaarlijker dan een taak die luid faalt, want je merkt het pas als het te laat is.
Log wat er gebeurt. Met Start-Transcript leg je de volledige uitvoer vast in een bestand. Bij een incident of een storing wil je kunnen terugzien wat de taak die nacht precies deed. Stuur belangrijke gebeurtenissen eventueel ook naar het Windows-eventlog, zodat ze in je monitoring terechtkomen.
Draai voorspelbaar. De vlaggen -NoProfile en -NonInteractive zorgen dat het script niet afhankelijk is van een gebruikersprofiel en nooit blijft wachten op invoer. Een scheduled task die op een prompt wacht, hangt tot in de eeuwigheid.
Zet je scripts in versiebeheer. Een script in Git heeft een geschiedenis: je ziet wie wat wanneer veranderde, en je kunt terug naar een vorige versie. Dat is ook een beveiligingsmaatregel, want een onverwachte wijziging valt zo meteen op.
Test voordat je plant. Draai het script eerst handmatig onder het serviceaccount, niet onder je eigen account. Veel taken werken wél als jij ze start en níét als de geplande taak ze draait, juist door een verschil in rechten of in toegang tot het wachtwoordbestand.
Samengevat
Begin bij een apart serviceaccount met minimale rechten, en gebruik een gMSA als dat kan. Onderteken je scripts zodat ze niet ongemerkt aangepast kunnen worden. Moet er toch een wachtwoord op schijf, bescherm het dan met DPAPI én een strakke ACL, of gebruik SecretManagement. En zorg dat de taak logt, netjes faalt en in versiebeheer staat. Dan draait je automatisering niet alleen, maar ook veilig.