All posts

Run a PowerShell scheduled task with a gMSA

How can a PowerShell job run without an entered password and prove its destination access?

A scheduled PowerShell task can use a group Managed Service Account without anyone entering its password. Configure the principal as DOMAIN\account$ with -LogonType Password, choose the executable explicitly, and test the job under that identity. The word Password is the logon type; it is not a password value.

This walkthrough writes a small JSON file to an SMB share and reads it back. It records the account, runtime and result locally, so a successful registration cannot be mistaken for a successful export.

Prepare the account and three separate permissions

Start with an existing gMSA, an authorized domain-joined execution host and the ActiveDirectory module. The gMSA setup guide covers account creation and the KDS prerequisites. On the execution host, run an elevated Windows PowerShell session:

Import-Module ActiveDirectory
Install-ADServiceAccount -Identity 'gmsaExport'
Test-ADServiceAccount -Identity 'gmsaExport'

Continue when the test returns True. If it fails, first check host authorization and the prerequisites in the gMSA setup guide. Microsoft documents this as a test of the account on the local computer, not of the export destination: Test-ADServiceAccount.

PermissionIdentity receiving itPurpose
Retrieve the managed passwordExecution host's computer account, usually through a groupUse the gMSA on this host
Log on as a batch jobCONTOSO\gmsaExport$Start the scheduled process
Script, log and destination accessCONTOSO\gmsaExport$Perform this specific job

Check the effective Log on as a batch job policy and Deny log on as a batch job, including group membership. Apply any required grant through the policy managing this host; do not replace the existing list of accounts. The deny right takes precedence. See Microsoft's batch-logon policy documentation.

Prepare a dedicated test share, here \\files01.contoso.com\GmsaProbe, containing no production data. Give the gMSA Change on that share and Modify on its backing test directory. Check both layers. The account does not need to be a local administrator. Use a DNS name and a UNC path rather than a drive mapped in your personal session.

On the execution host, create a new C:\Ops\GmsaProbe directory and a Results subdirectory. Configure their ACLs as follows; keep the script directory separate from writable output:

Local pathSYSTEM and AdministratorsgMSA
C:\Ops\GmsaProbe and the scriptFull controlRead and execute
C:\Ops\GmsaProbe\ResultsFull controlModify

Remove inherited write access for ordinary users on this dedicated directory. The gMSA must not be able to replace its own script. The local report directory must remain writable even when the network destination is unavailable.

Save a probe that reports its own result

Save this as C:\Ops\GmsaProbe\Probe.ps1. Use the execution policy and signing procedure required by your environment. The example does not change the execution policy or add a bypass switch.

[CmdletBinding()]
param([Parameter(Mandatory = $true)][string]$Destination)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
$runId = [guid]::NewGuid().ToString()
$record = [ordered]@{
    runId = $runId
    startedUtc = [DateTime]::UtcNow.ToString('o')
    identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
    powershell = $PSVersionTable.PSVersion.ToString()
    executable = (Get-Process -Id $PID).Path
    destination = $Destination
    success = $false
    error = $null
}
$exitCode = 1
try {
    $target = Join-Path $Destination ($runId + '.json')
    $payload = [ordered]@{ runId = $runId; identity = $record.identity }
    $payload | ConvertTo-Json | Set-Content -LiteralPath $target -Encoding UTF8
    $readBack = Get-Content -LiteralPath $target -Raw | ConvertFrom-Json
    if ($readBack.runId -ne $runId) { throw 'Read-back does not match this run.' }
    $record.success = $true
    $exitCode = 0
} catch {
    $record.error = $_.Exception.Message
} finally {
    $reportPath = Join-Path $PSScriptRoot ('Results\' + $runId + '.json')
    $record | ConvertTo-Json | Set-Content -LiteralPath $reportPath -Encoding UTF8
}
exit $exitCode

Each attempt has its own run ID. A missing local report means that execution or local logging still needs investigation; it is not an empty success. For a real export, replace the probe with acceptance checks for the actual data and define retention for the output and logs.

Register the task without supplying a password

Run these commands as an administrator on the execution host after adapting the domain and destination:

$action = New-ScheduledTaskAction `
    -Execute "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" `
    -Argument '-NoProfile -NonInteractive -File "C:\Ops\GmsaProbe\Probe.ps1" -Destination "\\files01.contoso.com\GmsaProbe"' `
    -WorkingDirectory 'C:\Ops\GmsaProbe'
$principal = New-ScheduledTaskPrincipal `
    -UserId 'CONTOSO\gmsaExport$' -LogonType Password -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName 'GmsaExportProbe' `
    -Action $action -Principal $principal -Settings $settings

There is deliberately no time trigger yet: first verify an on-demand run. There is also no -Force to silently replace an existing task. The two-minute limit belongs to this small probe, not to every production export.

ServiceAccount is not the appropriate logon type merely because gMSA contains “service account” in its name: Microsoft's enum describes that value for LocalSystem, LocalService and NetworkService. S4U also has different network-access semantics. See Principal.LogonType and New-ScheduledTaskPrincipal. The gMSA/Password combination above was verified in the lab without a password argument.

This task explicitly starts Windows PowerShell 5.1. For a PowerShell 7 job, select the installed pwsh.exe path and repeat the acceptance tests with its modules. Installing PowerShell 7 does not change an existing task's executable. The runtime comparison explains what to verify.

Verify a new run rather than an old result

Record the previous run time, start the task and wait for that new run to finish:

$previousRun = (Get-ScheduledTaskInfo -TaskName 'GmsaExportProbe').LastRunTime
Start-ScheduledTask -TaskName 'GmsaExportProbe'
$deadline = (Get-Date).AddSeconds(45)
do {
    Start-Sleep -Milliseconds 400
    $info = Get-ScheduledTaskInfo -TaskName 'GmsaExportProbe'
    $task = Get-ScheduledTask -TaskName 'GmsaExportProbe'
} while (($info.LastRunTime -le $previousRun -or $task.State -eq 'Running') `
    -and (Get-Date) -lt $deadline)
if ($info.LastRunTime -le $previousRun -or $task.State -eq 'Running') {
    throw 'No completed new run within 45 seconds; inspect the task.'
}
$info | Select-Object LastRunTime, LastTaskResult

The 45-second timeout only stops this observation; it does not cancel the task. In the resulting local JSON, check identity, executable, success and error. Match its runId to the file on the share, inspect that file's content and check its owner on the file server. A result of 0 is meaningful here because the probe returns it only after writing and reading back its own file.

The lab produced these contrasting results:

CaseAccount testTask resultEvidence
gMSA has destination accessTrue0Matching file, gMSA owner, local success report
gMSA lacks NTFS access to the test destinationTrue1Local report records access denied

Both runs used the gMSA without administrator membership. Changing the destination permission changed the job result, not the account test. Use a separate test directory for a negative test; do not revoke access from a production job to reproduce it.

Only after acceptance, add a schedule, for example:

$trigger = New-ScheduledTaskTrigger -Daily -At '02:00'
Set-ScheduledTask -TaskName 'GmsaExportProbe' -Trigger $trigger

Confirm the host's local time zone and the intended handling of missed starts. This example does not enable catch-up execution.

Remove the probe and keep the useful evidence

When the probe is no longer needed, disable it, check whether it is still running and stop only this test task if necessary. Then unregister it:

Disable-ScheduledTask -TaskName 'GmsaExportProbe'
Get-ScheduledTask -TaskName 'GmsaExportProbe' | Select-Object TaskName, State
# Only if this test task is still running:
# Stop-ScheduledTask -TaskName 'GmsaExportProbe'
Unregister-ScheduledTask -TaskName 'GmsaExportProbe' -Confirm:$true

Archive the test reports and remove the dedicated files, share and permissions when no longer needed. Uninstall or remove the gMSA only after checking its other consumers.

For one independent job, Task Scheduler may be sufficient. When exports acquire dependencies, retries and a shared operational history, PowerShell workflows in NodePilot become a relevant next step. Verify execution identity and destination permissions again in that actual runtime.

Back to the blogGo to the documentation