Note that absolutely none of this is authoritative or directly based on relevant documentation. It’s mostly what I found and figured out and guessed and (in some cases) made up. Some of it may be wrong or dangerous or lead to disaster or confusion. I am not taking responsibility here for anything. Read and act on it at your own peril!

As partly explained in the post on Scheduled Tasks, setting permissions on scheduled tasks is not trivial.

So what is more obvious than avoiding those permissions using Just Enough Admin again?

The immediate problem is that allowing non-privileged users to create and modify scheduled tasks using a virtual administrator account will allow those users to create scheduled tasks running as LocalSystem that will then do whatever those users want, perhaps just add them to the Administrators group.

Any JEA configuration for scheduled tasks would have to disallow creation or modification of scheduled tasks that use privileged accounts. Perhaps it is sufficient to stop creation of scheduled tasks that run as a system account (LocalSystem, LocalService, or NetworkService). Scheduled tasks to be modified can then simply be recreated, then being subject to the same restriction.

This requires two cmdlets to be available:

  1. Get-ScheduledTask to get any scheduled task as a PowerShell object (so that it can then be modified in PowerShell)
  2. Register-RestrictedScheduledTask as a wrapper for Register-ScheduledTask that makes the -Password parameter a required parameter (to disallow creation of scheduled tasks that use accounts without passwords)

As a bonus, another cmdlet can annex and permission a scheduled task for the calling user, to allow manipulation in the Task Scheduler GUI (taskschd.msc) as a non-privileged user.

  1. Claim-ScheduledTask changes the owner of a scheduled task to the calling user and adds the user to the scheduled task’s ACL with Full Access before any other entry (to preempt deny entries as well, also, it’s easier)

Create a ScheduledTasks.pssc and the associated JEA group:

PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-LocalGroup JEA_ScheduledTasks

Name               Description
----               -----------
JEA_ScheduledTasks


PS C:\Program Files\WindowsPowerShell\Modules\JEA> Add-LocalGroupMember JEA_ScheduledTasks benoit
PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-PSSessionConfigurationFile ScheduledTasks.pssc -SessionType RestrictedRemoteServer -RunAsVirtualAccount -RoleDefinitions @{'JEA_ScheduledTasks'=@{'RoleCapabilities'='ScheduledTasks'}}
PS C:\Program Files\WindowsPowerShell\Modules\JEA> Get-Content .\ScheduledTasks.pssc
@{

# Version number of the schema used for this document
SchemaVersion = '2.0.0.0'

# ID used to uniquely identify this document
GUID = '93c0a5f4-0e2b-4b52-8ec5-cde3c5b83837'

# Author of this document
Author = 'ajbrehm'

# Description of the functionality provided by these settings
# Description = ''

# Session type defaults to apply for this session configuration. Can be 'RestrictedRemoteServer' (recommended), 'Empty', or 'Default'
SessionType = 'RestrictedRemoteServer'

# Directory to place session transcripts for this session configuration
# TranscriptDirectory = 'C:\Transcripts\'

# Whether to run this session configuration as the machine's (virtual) administrator account
RunAsVirtualAccount = $true

# Scripts to run when applied to a session
# ScriptsToProcess = 'C:\ConfigData\InitScript1.ps1', 'C:\ConfigData\InitScript2.ps1'

# User roles (security groups), and the role capabilities that should be applied to them when applied to a session
RoleDefinitions = @{
    'JEA_ScheduledTasks' = @{
        'RoleCapabilities' = 'ScheduledTasks' } }
}
PS C:\Program Files\WindowsPowerShell\Modules\JEA>

Also create a plain RoleCapability file:

PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-PSRoleCapabilityFile .\RoleCapabilities\ScheduledTasks.psrc
PS C:\Program Files\WindowsPowerShell\Modules\JEA>

And edit the file thusly:

@{

# ID used to uniquely identify this document
GUID = 'ef187b87-dff9-4a6e-80a2-c9dabde880d7'

# Author of this document
Author = 'ajbrehm'

# Description of the functionality provided by these settings
# Description = ''

# Company associated with this document
CompanyName = 'Unknown'

# Copyright statement for this document
Copyright = '(c) 2026 ajbrehm. All rights reserved.'

# Modules to import when applied to a session
# ModulesToImport = 'MyCustomModule', @{ ModuleName = 'MyCustomModule'; ModuleVersion = '1.0.0.0'; GUID = '4d30d5f0-cb16-4898-812d-f20a6c596bdf' }
ModulesToImport = 'ScheduledTasks'

# Aliases to make visible when applied to a session
# VisibleAliases = 'Item1', 'Item2'

# Cmdlets to make visible when applied to a session
# VisibleCmdlets = 'Invoke-Cmdlet1', @{ Name = 'Invoke-Cmdlet2'; Parameters = @{ Name = 'Parameter1'; ValidateSet = 'Item1', 'Item2' }, @{ Name = 'Parameter2'; ValidatePattern = 'L*' } }
VisibleCmdlets = 'Get-ScheduledTask','Get-Help'

# Functions to make visible when applied to a session
# VisibleFunctions = 'Invoke-Function1', @{ Name = 'Invoke-Function2'; Parameters = @{ Name = 'Parameter1'; ValidateSet = 'Item1', 'Item2' }, @{ Name = 'Parameter2'; ValidatePattern = 'L*' } }
VisibleFunctions = 'Register-RestrictedScheduledTask','Get-ScheduledTaskPermissions','Claim-ScheduledTask'

# External commands (scripts and applications) to make visible when applied to a session
# VisibleExternalCommands = 'Item1', 'Item2'

# Providers to make visible when applied to a session
# VisibleProviders = 'Item1', 'Item2'
VisibleProviders = 'FileSystem'

# Scripts to run when applied to a session
# ScriptsToProcess = 'C:\ConfigData\InitScript1.ps1', 'C:\ConfigData\InitScript2.ps1'

# Aliases to be defined when applied to a session
# AliasDefinitions = @{ Name = 'Alias1'; Value = 'Invoke-Alias1'}, @{ Name = 'Alias2'; Value = 'Invoke-Alias2'}

# Functions to define when applied to a session
# FunctionDefinitions = @{ Name = 'MyFunction'; ScriptBlock = { param($MyInput) $MyInput } }
FunctionDefinitions = @(
   @{
        Name = 'Register-RestrictedScheduledTask'
        ScriptBlock = {
            param(
                [Parameter(mandatory = $true)]
                [string]$TaskName,
                [Parameter(mandatory = $true)]
                [string]$User,
                [Parameter(mandatory = $true)]
                [string]$Password,
                [Parameter(mandatory = $true)]
                [object]$InputObject,
                [switch]$Force
            )
            if ($Force) {
                ScheduledTasks\Register-ScheduledTask -TaskName $TaskName -User $User -Password $Password -InputObject $InputObject -Force
            } else {
                ScheduledTasks\Register-ScheduledTask -TaskName $TaskName -User $User -Password $Password -InputObject $InputObject
            }#if
        }
    },
    @{
        Name = 'Get-ScheduledTaskPermissions'
        ScriptBlock = {
            param(
                [string]$TaskName
            )
            $task = Get-ScheduledTask $TaskName
            $schedule = New-Object -ComObject 'Schedule.Service'
            $schedule.Connect()
            $folder = $schedule.GetFolder('\')
            $task = $folder.GetTask($task.URI)
            $sddl = $task.GetSecurityDescriptor(7)
            $sddl
        }
    },
    @{
        Name = 'Claim-ScheduledTask'
        ScriptBlock = {
            param(
                [string]$TaskName,
                [switch]$Force
            )
            $task = Get-ScheduledTask $TaskName
            $schedule = New-Object -ComObject 'Schedule.Service'
            $schedule.Connect()
            $folder = $schedule.GetFolder('\')
            $task = $folder.GetTask($task.URI)
            $sddl = $task.GetSecurityDescriptor(7)
            $sid = ([System.Security.Principal.NTAccount]$PSSenderInfo.ConnectedUser).Translate([System.Security.Principal.SecurityIdentifier])
            $sddl = $sddl -replace "^O:.*G:","O:${sid}G:"
            $ace = "(A;;FA;;;${sid})"
            if (!$sddl.Contains($ace)) {
                $sddl = $sddl -replace "D:","D:${ace}"
            }#if
            if (!$Force) {
                Read-Host "Writing security descriptor [$sddl] into scheduled task [$TaskName]. Press ctrl+c to cancel."
            }#if
            $task.SetSecurityDescriptor($sddl,0)
        }
    }
)
# Variables to define when applied to a session
# VariableDefinitions = @{ Name = 'Variable1'; Value = { 'Dynamic' + 'InitialValue' } }, @{ Name = 'Variable2'; Value = 'StaticInitialValue' }

# Environment variables to define when applied to a session
# EnvironmentVariables = @{ Variable1 = 'Value1'; Variable2 = 'Value2' }

# Type files (.ps1xml) to load when applied to a session
# TypesToProcess = 'C:\ConfigData\MyTypes.ps1xml', 'C:\ConfigData\OtherTypes.ps1xml'

# Format files (.ps1xml) to load when applied to a session
# FormatsToProcess = 'C:\ConfigData\MyFormats.ps1xml', 'C:\ConfigData\OtherFormats.ps1xml'

# Assemblies to load when applied to a session
# AssembliesToLoad = 'System.Web', 'System.OtherAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

}

Note that this configuration gives access to two cmdlets Get-ScheduledTask and Get-Help. The latter is included to allow showing help for the two exposed functions.

It also gives access to three functions Register-RestrictedScheduledTask, Get-ScheduledTaskPermissions, and Claim-ScheduledTask. (The latter cmdlet might need a cooler name.)

Register-RestrictedScheduledTask is simple: it just calls Register-ScheduledTask but will insist that the -Password parameter exists and that something is given as argument for it. (See Parameters and Arguments. It’s never quite clear to me.) Since system accounts don’t take a password, the user cannot register a scheduled task that runs as a system account. (The user can still register a scheduled task that runs as another privileged account, like a member of the Administrators group, but if the user knows such an account, then why does he bother using this JEA configuration?)

Get-ScheduledTaskPermissions uses a COM (Component Object Model) API to read scheduled task permissions and gets an SDDL (Security Descriptor Definition Language) representation of the scheduled task’s security descriptor which it will then produly display.

Claim-ScheduledTask is a bit pseudo-clever. It also uses the COM API to modify scheduled task permissions (which is otherwise only possible via direct registry access). It gets an SDDL representation of the scheduled task’s security descriptor and modifies it by a) replacing the owner and b) inserting an entry at the beginning of the access control list giving the new owner full access. Then it writes the SDDL back into the scheduled task COM object.

Note that you understood correctly: This does allow the user to annex any scheduled task he wants. (Modify the cmdlet code if you want to prevent this.) But he cannot change what it does and and keep it running under an account of which he does not know the password. So, yes, the user can cause damage with this cmdlet. Deploy with care!

Register the session configuration:

PS C:\Program Files\WindowsPowerShell\Modules\JEA> Register-PSSessionConfiguration ScheduledTasks -Path .\ScheduledTasks.pssc
WARNING: Register-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a restart of WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.


   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin

Type            Keys                                Name
----            ----                                ----
Container       {Name=ScheduledTasks}               ScheduledTasks
WARNING: Set-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a restart of WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.
WARNING: Register-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a restart of WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.


PS C:\Program Files\WindowsPowerShell\Modules\JEA>

Make sure your user is in both the Remote Management Users and JEA_ScheduledTasks groups and:

PS C:\> whoami
achaemenes\benoit
PS C:\> whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                          State
============================= ==================================== ========
SeShutdownPrivilege           Shut down the system                 Disabled
SeChangeNotifyPrivilege       Bypass traverse checking             Enabled
SeUndockPrivilege             Remove computer from docking station Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set       Disabled
SeTimeZonePrivilege           Change the time zone                 Disabled
PS C:\> $scheduledtasks=New-PSSession -ConfigurationName ScheduledTasks
PS C:\> $action=New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c cls"
PS C:\> $task=New-ScheduledTask -Action $action -Settings (New-ScheduledTaskSettingsSet)
PS C:\> Invoke-Command $scheduledtasks {param($task); Register-RestrictedScheduledTask -TaskName BenoitTask -User benoit -InputObject $task} -ArgumentList $task

cmdlet Register-RestrictedScheduledTask at command pipeline position 1
Supply values for the following parameters:
Password: BenoitsSecretPassword

TaskPath                                       TaskName                          State      PSComputerName
--------                                       --------                          -----      --------------
\                                              BenoitTask                        Ready      localhost

PS C:\> Invoke-Command $scheduledtasks {Get-ScheduledTask -TaskName BenoitTask}

TaskPath                                       TaskName                          State      PSComputerName
--------                                       --------                          -----      --------------
\                                              BenoitTask                        Ready      localhost

PS C:\> Invoke-Command $scheduledtasks {Get-ScheduledTaskPermissions -TaskName BenoitTask}
O:S-1-5-94-3G:S-1-5-94-3D:(A;ID;0x1f019f;;;BA)(A;ID;0x1f019f;;;SY)(A;ID;FA;;;S-1-5-94-3)(A;;FR;;;S-1-5-21-2059049455-1877585131-3415813230-1007)
PS C:\> Invoke-Command $scheduledtasks {Claim-ScheduledTask -TaskName BenoitTask}
Writing security descriptor [O:S-1-5-21-2059049455-1877585131-3415813230-1007G:S-1-5-94-3D:(A;;FA;;;S-1-5-21-2059049455-1877585131-3415813230-1007)(A;ID;0x1f019f;;;BA)(A;ID;0x1f019f;;;SY)(A;ID;FA;;;S-1-5-94-3)(A;;FR;;;S-1-5-21-2059049455-1877585131-3415813230-1007)] into scheduled task [BenoitTask]. Press ctrl+c to cancel.:

PS C:\> Invoke-Command $scheduledtasks {Get-ScheduledTaskPermissions -TaskName BenoitTask}
O:S-1-5-21-2059049455-1877585131-3415813230-1007G:S-1-5-94-3D:AI(A;;FA;;;S-1-5-21-2059049455-1877585131-3415813230-1007)(A;;FR;;;S-1-5-21-2059049455-1877585131-3415813230-1007)(A;ID;0x1f019f;;;BA)(A;ID;0x1f019f;;;SY)(A;ID;FA;;;S-1-5-94-3)
PS C:\>

Observe how user benoit can register a scheduled task. With the the -Force parameter set, it will also overwrite an existing scheduled task.

To modify a task, benoit can Get-ScheduledTask it, modify the task object and then Register-RestrictedScheduledTask -Force it:

PS C:\> $task1=Invoke-Command $scheduledtasks {Get-ScheduledTask -TaskName BenoitTask}
PS C:\> $task1

TaskPath                                       TaskName                          State      PSComputerName
--------                                       --------                          -----      --------------
\                                              BenoitTask                        Ready      localhost

PS C:\> $task1.Actions

Id               :
Arguments        : /c cls
Execute          : cmd.exe
WorkingDirectory :
PSComputerName   :

PS C:\> $action=New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c echo foo"
PS C:\> $task1.Actions=$action

TaskPath                                       TaskName                          State      PSComputerName
--------                                       --------                          -----      --------------
\                                              BenoitTask                        Ready      localhost

PS C:\> $task1.Actions

Id               :
Arguments        : /c echo foo
Execute          : cmd.exe
WorkingDirectory :
PSComputerName   :

PS C:\> Invoke-Command $scheduledtasks {param($task); Register-RestrictedScheduledTask -TaskName BenoitTask -User benoit -Password BenoitsSecretPassword -InputObject $task -Force} -ArgumentList $task

TaskPath                                       TaskName                          State      PSComputerName
--------                                       --------                          -----      --------------
\                                              BenoitTask                        Ready      localhost


PS C:\>

The task now does a “cmd.exe /c echo foo” instead of the original “cmd.exe /c cls”. (But it is running as the account given to it when registering it after modification.)

And if user benoit needs to have access to this (or any other) scheduled task in the Task Scheduler GUI (or with PowerShell but without JEA) Claim-ScheduledTask annexes the scheduled task to him and grants him access.

Note again that absolutely none of this is authoritative or directly based on relevant documentation. It’s mostly what I found and figured out and guessed and (in some cases) made up. Some of it is wrong or dangerous or will lead to disaster or confusion. I am not taking responsibility here for anything.

Happy new year, everyone!

P.S.: In case you ever wondered. The “Run with highest privileges” checkbox in Task Scheduler does nothing to non-interactive scheduled tasks (batch jobs). What it does do is affect interactive scheduled tasks. It only becomes relevant when “Run only when user is logged on” is selected (instead of “Run whether user is logged on or not”). It acts as a preemptive OK to a UAC prompt that might otherwise prevent the interactive process from running elevated. I don’t know where this is properly documented.

Next: TBD