Post-Rotation scripts for rotating the credential of a Windows scheduled task
Overview
This example will allow you to rotate the credential on a Windows Scheduled Task running as a Service Account that has its password rotated via Keeper PAM.
Using PowerShell Scripts
Prerequisites
To use these scripts, PowerShell 7 must be available on the target machine and should be set up and configured to enable remoting using PowerShell 7 using Enable-PSRemoting.
Pulling Parameters from the Record
The data in the record being rotated is made available to your script via a BASE64-encoded JSON string. This is passed into your script for consumption. When your script has finished execution, Clear-History is executed to ensure that the record data is not available for future PowerShell sessions.
# The Gateway will execute your script as follows"BASE64STRING=="| .\your-script.ps1; Clear-History
Using Batch Files
Prerequisites
The Remote Procedure Call (RPC) and Windows Management Instrumentation services should be enabled and running on the target server to run the scripts in the examples below.
To rotate the credential of a service account, the user (which in this case is the Gateway's user account) will need to be part of the Administrator's group on the target machine. This means the Gateway must run as a Service account that is assigned the appropriate level of privilege to achieve this and not run as the default SYSTEM user.
This example uses the commonly used tool jq, for parsing the JSON data passed to the script containing the records data. This example assumes you have it installed and the jq command is in PATH.
Pulling Parameters from the Record
The data in the record being rotated is made available to your script via a BASE64-encoded JSON string. This is passed into your script for consumption.
# The Gateway will execute your script as follows"BASE64STRING=="| .\your-script.bat && echo ####RC %errorlevel%
PowerShell Example over WinRM
PAM script for updating the "Log on as" credentials of a Windows Scheduled Task
Overview
The following PowerShell code example updates the "Log on as" credentials of a Windows Scheduled Task after its password has been rotated via Keeper Rotation. These scripts are the preferred method of performing a "Log on as" credential update on either the local Keeper Gateway host or target Windows Based systems.
Example:
In this example, we will be using the "Invoke-Method" over an established "PSSession" to a Windows based target system and utilizes the native PowerShell cmdlets for managing a Windows Scheduled Task. In this case we are using the "Microsoft.PowerShell.Management" and its sub-commands like, "Set-ScheduledTask", to manage the "Log on as" credentials of a target Windows Scheduled Task.
Step 1:
Expand and copy the PowerShell script, below, into a ",ps1" filename of your choosing. In this example, we named the script filename "update_tasksched_cred.ps1". Attach the script to the "PAM Scripts" section of the PAM User record. Include two additional "Rotation Credential" records by selecting the current PAM User record and the Administrative Credential record that will run the script. The Administrative Credential can be the same one used in the "Password Rotation Settings" or another record which has credentials with the correct administrative permissions to run the script and its actions.
PowerShell Script using Invoke-Method over PSSession and Microsoft.PowerShell.Management
[CmdletBinding()]param ( [Parameter(ValueFromPipeline=$true)] [string] $Record)try {# This Section brings in the PAM User Record InformationWrite-Debug"Decoding and converting the PAM User Record Information from Base64" $RecordJsonAsB64 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Record))if (-not $RecordJsonAsB64) {throw"Failed to decode the PAM User Record Information from Base64." }Write-Debug"Converting the decoded JSON to PowerShell object" $RecordParams = $RecordJsonAsB64 |ConvertFrom-Jsonif (-not $RecordParams) {throw"Failed to convert the decoded JSON to PowerShell object." }Write-Debug"PAM User Record Information successfully retrieved and converted."}catch {Write-Error"An error occurred while processing the PAM User Record Information: $_"}finally {Write-Debug"Completed processing the PAM User Record Information."}# End of Section.try {# This Section brings in ALL associated Records and their Parameter InformationWrite-Debug"Decoding and converting all associated records from Base64" $recordsJSON = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($RecordParams.records))if (-not $recordsJSON) {throw"Failed to decode the associated records from Base64." }Write-Debug"Converting the decoded JSON to PowerShell object" $records = $recordsJSON |ConvertFrom-Jsonif (-not $records) {throw"Failed to convert the decoded JSON to PowerShell object." }Write-Debug"Associated records successfully retrieved and converted."}catch {Write-Error"An error occurred while processing the associated records: $_"}finally {Write-Debug"Completed processing the associated records."}# End of Section.try {# This Section Defines parameters from the User Record. "remotecomp" and "taskname" are coming from custom fields in the Pam User RecordWrite-Debug"Defining parameters from the User Record" $ErrorActionPreference ='Stop' $DebugPreference ='Continue' $remoteComputer = ($records |Where-Object {$_.uid-eq $RecordParams.userRecordUid}).remotecompif (-not $remoteComputer) {throw"Failed to retrieve 'remotecomp' from the User Record." } $taskName = ($records |Where-Object {$_.uid-eq $RecordParams.userRecordUid}).tasknameif (-not $taskName) {throw"Failed to retrieve 'service' from the User Record." } $user = ($RecordParams.user)if (-not $user) {throw"Failed to retrieve 'user' from the User Record." } $newPassword = ($RecordParams.newPassword)if (-not $newPassword) {throw"Failed to retrieve 'newPassword' from the User Record." }Write-Debug"Parameters from the User Record successfully defined."}catch {Write-Error"An error occurred while defining parameters from the User Record: $_"}finally {Write-Debug"Completed defining parameters from the User Record."}# End of Section.try {# This Section Defines your AD Administrative credentials from the Resource RecordWrite-Debug"Defining AD Administrative credentials from the Resource Record" $adAdminUser = ($records |Where-Object {$_.uid-eq $RecordParams.resourceRecordUid}).loginif (-not $adAdminUser) {throw"Failed to retrieve 'login' from the Resource Record." } $adAdminPassword = ($records |Where-Object {$_.uid-eq $RecordParams.resourceRecordUid}).passwordif (-not $adAdminPassword) {throw"Failed to retrieve 'password' from the Resource Record." }Write-Debug"AD Administrative credentials successfully defined."}catch {Write-Error"An error occurred while defining AD Administrative credentials from the Resource Record: $_"}finally {Write-Debug"Completed defining AD Administrative credentials from the Resource Record."}# End of Section.# The above calls the Resrouce Record NOT any attached record to the script. Below is an example of calling out an atttached record to the script.# $adAdminUser = ($records | Where-Object {$_.uid -eq "UID of Record Here"}).login# $adAdminPassword = ($records | Where-Object {$_.uid -eq "UID of Record Here"}).passwordtry {# This Section further secures the AD administrative credentials into a Secure StringWrite-Debug"Securing the AD administrative credentials into a Secure String" $securePassword =ConvertTo-SecureString $adAdminPassword -AsPlainText -Forceif (-not $securePassword) {throw"Failed to convert the administrative password to a Secure String." } $credential =New-Object System.Management.Automation.PSCredential ("$adAdminUser", $securePassword)if (-not $credential) {throw"Failed to create PSCredential object." }Write-Debug"AD administrative credentials successfully secured and PSCredential object created."}catch {Write-Error"An error occurred while securing AD administrative credentials: $_"}finally {Write-Debug"Completed securing AD administrative credentials."}# End of Section.try {# This Section creates a new PSSession with the AD Administrative Resource RecordWrite-Debug"Creating a new PSSession with the AD Administrative Resource Record" $session =New-PSSession-ComputerName $remoteComputer -Credential $credential -ErrorAction Stopif (-not $session) {throw"Failed to create a new PSSession." }Write-Debug"PSSession created successfully." }catch {Write-Error"An error occurred while creating the PSSession: $_" }finally {Write-Debug"Completed the attempt to create a new PSSession." }# End of Section.try {# This Script Block updates the password for a given Scheduled Task.Write-Debug"Updating the password for the scheduled task: $taskName" $result =Invoke-Command-Session $session -ScriptBlock {param ($taskName, $user, $newPassword)try {Write-Debug"Setting the new password for scheduled task: $taskName"Set-ScheduledTask-TaskName "$taskName"-User "$user"-Password "$newPassword"-ErrorAction StopWrite-Output"Password for scheduled task $taskName has been updated successfully." }catch {Write-Error"An error occurred while updating the password for scheduled task "$taskName": $_"throw$_ } } -ArgumentList $taskName, $user, $newPassword -ErrorAction StopWrite-Debug"Result of the remote command: $result"}catch {Write-Error"An error occurred while updating the password for the scheduled task on the remote computer: $_"}finally {Write-Debug"Completed attempt to update the password for the scheduled task on the remote computer."}# This Sections Removes the PSSessionRemove-PSSession-Session $session# End of Section
The resulting PAM Script configuration screen is below:
Step 2:
Add two custom text fields, to the PAM User record, called "remotecomp" and "taskname". In the "remotecomp" field provide the DNS name of the target system which is running the Windows Scheduled Task. For this example "dc1" is the name of the target system. If the Windows Scheduled Task you are managing is local to the Keeper Gateway Host use, "localhost" for the "remotecomp" field. In the "taskname" field, provide the "Windows Scheduled Task" name of the task and not the the "Display Name". In this instance and example, the "Task Name" is "AutoSSH".
Here's the resulting Record configuration in the vault:
Running the Script:
You can utilize the "Rotate Now" button to run it "On-Demand" and test it out as well as set a schedule for the script in the "Password Rotation Settings"
PowerShell 7 Example over SSH
PAM script for updating the "Log on as" credentials of a Windows Scheduled Task.
PowerShell 7 and OpenSSH are required for this setup. If you have not installed PowerShell 7 on your Keeper Gateway host system and target Windows Based systems, please visit PowerShell 7 on Windows and OpenSSH on Windows for instructions.
To run this script, SSH key based authentication must be set up and enabled between the Keeper Gateway host system and the target systems. If you have not setup your key pair authentication, please visit the URL below for instructions.
The following PowerShell code example updates the "Log on as" credentials of a Windows Scheduled Task after its password has been rotated via Keeper Rotation over an established SSH session using PowerShell 7.
Example:
In this example, we will be using the "Invoke-Method" over an established "SSH" session to a Windows based target system and utilizes the native PowerShell 7 cmdlets for managing services. In this case we are using the "Microsoft.PowerShell.Management" and its sub-commands like, "Set-ScheduledTask", to manage the "Log on as" credentials of a target "Windows Scheduled Task".
Step 1:
Expand and copy the PowerShell script, below, into a ",ps1" filename of your choosing. In this example, we named the script filename "update_tasksched_cred.ps1". Attach the script to the "PAM Scripts" section of the PAM User record. Include one additional "Rotation Credential" record by selecting the current PAM User record. Check the box "Run with Command Prefix" and point to the path of your PowerShell 7 executable. This is a MUST and requirement for this script to function.
PowerShell Script using Invoke-Method over SSH
[CmdletBinding()]param ( [Parameter(ValueFromPipeline=$true)] [string] $Record)try {# This Section brings in the PAM User Record InformationWrite-Debug"Decoding and converting the PAM User Record Information from Base64" $RecordJsonAsB64 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Record))if (-not $RecordJsonAsB64) {throw"Failed to decode the PAM User Record Information from Base64." }Write-Debug"Converting the decoded JSON to PowerShell object" $RecordParams = $RecordJsonAsB64 |ConvertFrom-Jsonif (-not $RecordParams) {throw"Failed to convert the decoded JSON to PowerShell object." }Write-Debug"PAM User Record Information successfully retrieved and converted."}catch {Write-Error"An error occurred while processing the PAM User Record Information: $_"}finally {Write-Debug"Completed processing the PAM User Record Information."}# End of Section.try {# This Section brings in ALL associated Records and their Parameter InformationWrite-Debug"Decoding and converting all associated records from Base64" $recordsJSON = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($RecordParams.records))if (-not $recordsJSON) {throw"Failed to decode the associated records from Base64." }Write-Debug"Converting the decoded JSON to PowerShell object" $records = $recordsJSON |ConvertFrom-Jsonif (-not $records) {throw"Failed to convert the decoded JSON to PowerShell object." }Write-Debug"Associated records successfully retrieved and converted."}catch {Write-Error"An error occurred while processing the associated records: $_"}finally {Write-Debug"Completed processing the associated records."}# End of Section.try {# This Section Defines parameters from the User Record. "remotecomp" and "service" are coming from custom fields in the Pam User RecordWrite-Debug"Defining parameters from the User Record" $ErrorActionPreference ='Stop' $DebugPreference ='Continue' $remoteComputer = ($records |Where-Object {$_.uid-eq $RecordParams.userRecordUid}).remotecompif (-not $remoteComputer) {throw"Failed to retrieve 'remotecomp' from the User Record." } $serviceName = ($records |Where-Object {$_.uid-eq $RecordParams.userRecordUid}).serviceif (-not $serviceName) {throw"Failed to retrieve 'service' from the User Record." } $user = ($RecordParams.user)if (-not $user) {throw"Failed to retrieve 'user' from the User Record." } $newPassword = ($RecordParams.newPassword)if (-not $newPassword) {throw"Failed to retrieve 'newPassword' from the User Record." }Write-Debug"Parameters from the User Record successfully defined."}catch {Write-Error"An error occurred while defining parameters from the User Record: $_"}finally {Write-Debug"Completed defining parameters from the User Record."}# End of Section.try {# This Section creates a new PSSession with the AD Administrative Resource RecordWrite-Debug"Creating a new SSH Session with the AD Administrative Resource Record" $session =New-PSSession-HostName $remoteComputer -UserName $user -ErrorAction Stopif (-not $session) {throw"Failed to create a new SSH Session." }Write-Debug"SSH Session created successfully." }catch {Write-Error"An error occurred while creating the SSH Session: $_" }finally {Write-Debug"Completed the attempt to create a new SSH Session." }# End of Section.try {# This Script Block updates the password for a given Scheduled Task.Write-Debug"Updating the password for the scheduled task: $taskName" $result =Invoke-Command-Session $session -ScriptBlock {param ($taskName, $user, $newPassword)try {Write-Debug"Setting the new password for scheduled task: $taskName"Set-ScheduledTask-TaskName "$taskName"-User "$user"-Password "$newPassword"-ErrorAction StopWrite-Output"Password for scheduled task $taskName has been updated successfully." }catch {Write-Error"An error occurred while updating the password for scheduled task "$taskName": $_"throw$_ } } -ArgumentList $taskName, $user, $newPassword -ErrorAction StopWrite-Debug"Result of the remote command: $result"}catch {Write-Error"An error occurred while updating the password for the scheduled task on the remote computer: $_"}finally {Write-Debug"Completed attempt to update the password for the scheduled task on the remote computer."}# End Script Block.# This Sections Removes the SSH SessionRemove-PSSession-Session $session# End of Section
The resulting PAM Script configuration screen is below:
Step 2:
Add two custom text fields, to the PAM User record, called "remotecomp" and "taskname". In the "remotecomp" field provide the DNS name of the target system which is running the Windows Scheduled Task. For this example "dc1" is the name of the target system. If the Windows Scheduled Task you are managing is local to the Keeper Gateway Host use, "localhost" for the "remotecomp" field. In the "taskname" field, provide the "Windows Scheduled Task" name of the task and not the the "Display Name". In this instance and example, the "Task Name" is "AutoSSH".
Here's the resulting Record configuration in the vault:
Running the Script:
Great! If you are following along, just like with Keeper Password Rotations, you can utilize the "Rotate Now" button to run it "On-Demand" and test it out as well as set a schedule for the script in the "Password Rotation Settings"
PowerShell Example via WinRPC
Using Admin Credentials
To update the 'Log On As' property on a Windows Scheduled Task, you will need a credential with the appropriate permissions, such as an Administrator account.
When attaching a PAM script to a record, you have the option to add a Resource Credential that is passed to the Gateway as part of the BASE64-encoded JSON data. The above credential will need to be attached as a Resource Credential.
As many Resource Credentials can be attached to a PAM script, knowing the UID of the Resource Credential you have attached helps ensure your script uses the correct one to update the Service's 'Log On As' property.
Updating the Scheduled Task
You can use the schtasks command to update the credentials on the Scheduled Task. This command also requires the admin credentials mentioned above to perform the task.
Unfortunately, as the schtasks command is not a PowerShell cmdlet, so its output will not be captured by $error. Without additional error checking, regardless of the exit status of the schtasks command, the gateway will always show success. To solve for this, you can check $LastExitCode after each call to schtasks.
if( $LastExitCode-ne0 ) { exit$LastExitCode}
Full Example
[CmdletBinding()]param ( [Parameter(ValueFromPipeline=$true)] [string] $B64Input)$ErrorActionPreference ="Stop"$DebugPreference ='Continue'functionConvertFrom-B64 {param ( [string] $B64String )try { $Json = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($B64String)) $Output = $Json |ConvertFrom-Json }catch {Write-Error"Failed to convert Base64 string: $B64String" }return $Output}# The JSON data is passed to the Gateway as a Base64 encoded string.$Params =ConvertFrom-B64-B64String $B64InputWrite-Debug"Running Post-Rotation Script on: $($Params.userRecordUid)"# Convert the attached Resource Records from Base64 encoded JSON string and find the # Admin Record we need to update the Service's `Log On As` property by filtering by the # Admin Record's UID.$ResourceCredentials =ConvertFrom-B64-B64 $Params.records$AdminRecord = $ResourceCredentials |Where-Object { $_.uid-eq'<Admin Record UID>' }$AdminUserName ="$($AdminRecord.login)@$($AdminRecord.domainName)"$ScheduledTaskName ='<Scheduled Task Name>'Write-Debug"Updating Scheduled Task: $ScheduledTaskName"schtasks /change /tn $ScheduledTaskName /s '<Target Machine>'/u $AdminUserName /p $AdminRecord.password /ru $Params.user /rp $Params.newPasswordif( $LastExitCode-ne0 ) { exit$LastExitCode}
Batch Example via WinRPC
Parsing the piped input
As mentioned above, a BASE64 string will be piped into your script, which includes the username and new password (among other data), which you will use to rotate the Windows Scheduled Task credentials.
Using the below snippet, we can take the piped input and use certutil to decode the BASE64 string. These will be saved to temporary files and cleaned up later, as is the custom in bat scripts, as certutil only accepts files as input.
jq can be used on the resulting JSON file to get the values of user and newPassword.
for /f "usebackq delims=" %%a in (`jq -r .user %json%`) doset"user=%%a"for /f "usebackq delims=" %%a in (`jq -r .newPassword %json%`) doset"newPassword=%%a"
Using Admin Credentials
To update the 'Log On As' property on a Windows Scheduled Task, you will need a credential with the appropriate permissions, such as an Administrator account.
When attaching a PAM script to a record, you have the option to add a Resource Credential that is passed to the Gateway as part of the BASE64-encoded JSON data. The above credential will need to be attached as a Resource Credential.
As many Resource Credentials can be attached to a PAM script, knowing the UID of the Resource Credential you have attached helps ensure your script uses the correct one to update the Service's 'Log On As' property.
We can use jq to access the attached Resource Credential and filter by the records UID.
set adminrecord=%temp%\adminrecord.tmpset adminuid=<Admin UID>jq -r ".[] | select(.uid == \"%adminuid%\")" %recordsjson% > %adminrecord%@REM pull the login, domainName, and password from the %adminrecord% JSON objectfor /f "usebackq delims=" %%a in (`jq -r .login %adminrecord%`) doset"adminuser=%%a"for /f "usebackq delims=" %%a in (`jq -r .domainName %adminrecord%`) doset"domainname=%%a"for /f "usebackq delims=" %%a in (`jq -r .password %adminrecord%`) doset"adminpassword=%%a"@REM Create the admin usermain by combining the username@domainnameset adminusername=%adminuser%@%domainname%
Updating the Scheduled Task
The schtasks command is used to update the desired Scheduled Task using the values you just extracted. In addition to the new credentials, you will need the Admin credentials from above.