Using RPC for Secrets with Shared Credentials
In most environments, we recommend using a separate password for each account for optimal security. However in environments where identical credentials are used in multiple secrets, we recommend using RPC to change the password on one primary parent account secret, and then using a PowerShell dependency script to update values in child secrets. The PowerShell script calls back to Secret Server's API, retrieves a list of comma-separated values representing child secret IDs, and updates the values stored in the child secrets. We recommend using this process for no more than 25 child secrets.
Requirements
- A Secret Server instance version 10.1.000000 or newer with a premium add-on or Enterprise Plus
- A PowerShell implementation enabled and working properly. See Configuring WinRM for PowerShell
- The PowerShell Wellness Checker
For this procedure you will need to create the four types of user accounts listed below, and for each account you will need to create a corresponding secret in Secret Server with the account's login credentials and other information.
Create the user accounts and secrets described below:
- An API User account and a corresponding secret. This API User account will NOT take up a user license. Recommended templates for the secret include the Active Directory template and the Web Password template. Credentials may be a local account or an Active Directory service account assigned to the Synchronization group, but must be stored in Secret Server to be passed to the PowerShell script.
- A primary parent account and a corresponding secret that has RPC set up and the PowerShell dependency script from this page attached. The primary parent account credentials may be either a local account or an Active Directory service account assigned to the Synchronization group.
- Child accounts with a corresponding secret for each account containing the child secret ID, with edit permissions granted to the API User account.
- A privileged Active Directory account and a corresponding secret that can run PowerShell on the Secret Server machine.
To create a new dependency changer for synchronizing passwords during RPC, first:
-
On the server that the script will be processed from, whether DEs or web nodes, download the WellnessChecker tool ZIP file.
-
Extract the ZIP file and run this command:
PowerShell.WellnessChecker.exe -fixerrors
Then follow the steps below:
-
In Secret Server, browse to Admin > Scripts.
-
Click Create Script.
-
In the New Script dialog, fill in the fields for Name and Description, for the Script Type select PowerShell and specifyCategory.
-
Check Enabled next to the State option.
-
In the Script field, paste in the script provided at the bottom of this page.
-
Click Save at the bottom of the page to save the file.
-
Browse to Admin > Application, and make sure Enable Webservices is set to Yes.
-
Browse to the primary parent account secret and ensure that RPC is setup on it.
-
In the primary parent account secret, click the RPC tab.
-
Click Edit.
-
In the secret grid at the bottom, select the API User account secret you created. The API User account secret should be the only secret in the grid.
-
Browse to Admin > Discovery and click the Configuration tab.
-
Click Discovery Configuration Options and select Extensible Discovery from the drop-down list.
-
On the Extensible Discovery Configuration page, click Configure Dependency Changers.
-
On the Secret Dependency Changers page, click Create Dependency Changer.
-
In the New Dependency Changer dialog, enter the Name, check the State to Enabled, for Dependency Type select PowerShell script, for the Scan template select Computer Dependency (Basic), check the Create template checkbox.
-
In the Scripts section below enter the related Change Script, Verification Script, Change Success Script, and Change Fail Script.
-
In the Arguments field, paste the following:
$[1]$USERNAME $[1]$PASSWORD $PASSWORD $NOTES $[1]$DOMAIN
The actions of the Arguments are as follows:
-
$[1]$USERNAME
pulls the username from the privileged account on the primary parent account, which will be used to execute the PowerShell script. -
$[1]$PASSWORD
pulls the password from the associated secret on the primary parent account, which will be used to execute the PowerShell script. -
$PASSWORD
pulls the password from the primary parent account, which will be set for all secrets listed in the Notes field. -
$NOTES
pulls the Notes content from the primary parent account, and parses the comma separated list of secret IDs to find the other secrets to update. -
$[1]$DOMAIN
pulls the Domain field from the associated secret on the primary parent account. For local accounts, leave the Domain field on the associated secret empty. It must be listed last because of the way PowerShell parses empty fields.
The$[1]$DOMAIN
argument is not needed for Platform service user. -
-
Browse back to the Extensible Discovery Configuration page and this time, click Configure Dependency Templates.
-
On the Secret Dependency Templates Designer page, select the new dependency changer you configured in the last step.
-
Browse to the primary parent account secret and click the Dependencies tab.
-
Click New Dependency.
-
In the Create dependency dialog, click the Type dropdown and select the PowerShell dependency template you created.
-
In the Create dependency dialog, select Dependency group, enter New group name, enter New group site name, enter ServiceName, check the Enabled checkbox, enter
default
in the Machine Name field, and click Save. -
In the primary parent account secret's Notes field, ensure that the child secret IDs appear in a comma-separated-values list, for example
19,39,81...
Now the dependency has been added and you can test the full process by running a remote password change on the primary parent account. All of the secrets listed by ID in the Notes field should be updated with the same password.
PowerShell Script
$PASSWORD
token to a custom password changer PowerShell script, resulting in an authentication failure.Replace $baseUrl
with the name of the machine hosting your Secret Server instance.
$baseUrl = 'https://MySecretServerURL'
$apiUrl = "$baseUrl/api/v1"
$username = $Args[0]
$password = $Args[1]
$newpassword = $Args[2]
$secretIdArray = $Args[3]
$domain = $Args[4]
# Authentication - Get Access Token
$authUrl = "$baseUrl/oauth2/token"
$authBody = @{
username = $username
password = $password
grant_type = 'password'
}
# Add domain if provided
if ($domain) {
$authBody.domain = $domain
}
try {
$authResponse = Invoke-RestMethod -Uri $authUrl -Method Post -Body $authBody -
ContentType 'application/x-www-form-urlencoded'
$token = $authResponse.access_token
Write-Debug "Authentication successful"
} catch {
Write-Debug "Authentication failed: $($_.Exception.Message)"
exit 1
}
# Set up headers for authenticated requests
$headers = @{
'Authorization' = "Bearer $token"
'Content-Type' = 'application/json'
}
# Process each secret ID
$secretIds = $secretIdArray -split ","
foreach ($secretId in $secretIds) {
try {
# Get Secret
$getSecretUrl = "$apiUrl/secrets/$secretId"
$secret = Invoke-RestMethod -Uri $getSecretUrl -Method Get -Headers $headers
$secretName = $secret.name
Write-Debug "Updating Secret: $secretName"
# Find password fields and update them (matching original SOAP logic)
foreach ($item in $secret.items) {
if ($item.isPassword -eq $true) {
$item.itemValue = $newpassword
}
}
# Update Secret - pass the entire modified secret object
$updateSecretUrl = "$apiUrl/secrets/$secretId"
$updateBody = $secret | ConvertTo-Json -Depth 5
$updateResponse = Invoke-RestMethod -Uri $updateSecretUrl -Method Put -Headers
$headers -Body $updateBody
Write-Debug "Updated Secret: $secretName"
} catch {
Write-Debug "Error processing secret ID $secretId`: $($_.Exception.Message)"
continue
}
}
REST API Script for Platform
$platformTenant = "https://your-tenant.delinea.app"
$sscTenant = "https://your-tenant.secretservercloud.com"
$global:TOKEN_URL = "$platformTenant/identity/api/oauth2/token/xpmplatform"
$apiUrl = "$sscTenant/api/v1"
$global:CLIENT_ID = $Args[0]
$global:CLIENT_SECRET = $Args[1]
$newpassword = $Args[2]
$secretIdArray = $Args[3]
$global:SCOPE = "xpmheadless"
$global:GRANT_TYPE = "client_credentials" # Default grant type
$global:REFRESH_GRANT_TYPE = "refresh_token" # Grant type for refreshing the token
$global:API_URL = "$platformTenant/identity/api/entities/localusers" # Test API endpoint
$global:ACCESS_TOKEN = $null
$global:REFRESH_TOKEN = $null
$global:TOKEN_EXPIRES_AT = 0
function Get-InitialTokens {
$global:ACCESS_TOKEN
$global:REFRESH_TOKEN
$global:TOKEN_EXPIRES_AT
$payload = @{
grant_type = $GRANT_TYPE
client_id = $CLIENT_ID
client_secret = $CLIENT_SECRET
scope = $SCOPE
}
try {
$response = Invoke-RestMethod -Uri $TOKEN_URL -Method Post -Body $payload
if ($response) {
$global:ACCESS_TOKEN = $response.access_token
$global:REFRESH_TOKEN = $response.refresh_token
$expires_in = $response.expires_in
$global:TOKEN_EXPIRES_AT = (Get-Date).AddSeconds($expires_in).AddMinutes(-1)
Write-Host "Initial Access Token Obtained"
Write-Host "Access Token: $global:ACCESS_TOKEN"
if ($global:REFRESH_TOKEN) {
Write-Host "Refresh Token: $global:REFRESH_TOKEN"
}
}
} catch {
Write-Host "Failed to Obtain Initial Tokens"
Write-Host $_.Exception.Message
}
}
function Get-NewAccessToken {
$global:ACCESS_TOKEN
$global:TOKEN_EXPIRES_AT
$global:REFRESH_TOKEN
$headers = @{
Authorization = "Bearer $global:ACCESS_TOKEN"
}
$payload = @{
grant_type = $REFRESH_GRANT_TYPE
refresh_token = $global:REFRESH_TOKEN
}
try {
$response = Invoke-RestMethod -Uri $TOKEN_URL -Method Post -Body $payload -
Headers $headers
if ($response) {
$global:ACCESS_TOKEN = $response.access_token
$expires_in = $response.expires_in
$global:TOKEN_EXPIRES_AT = (Get-Date).AddSeconds($expires_in).AddMinutes(-1)
Write-Host "New Access Token Obtained"
Write-Host "Access Token: $global:ACCESS_TOKEN"
if ($response.refresh_token) {
$global:REFRESH_TOKEN = $response.refresh_token
Write-Host "Refresh Token: $global:REFRESH_TOKEN"
}
}
} catch {
Write-Host "Failed to Refresh Token"
Write-Host $_.Exception.Message
}
}
function Ensure-ValidToken {
if ((Get-Date) -gt $global:TOKEN_EXPIRES_AT) {
Write-Host "Access Token Expired"
Write-Host "Refreshing..."
Get-NewAccessToken
} else {
Write-Host "Access Token is Still Valid"
}
}
function Call-API {
$headers = @{
Authorization = "Bearer $global:ACCESS_TOKEN"
}
try {
$response = Invoke-RestMethod -Uri $API_URL -Method Get -Headers $headers
if ($response) {
Write-Host "API Call Successful"
Write-Host ($response | ConvertTo-Json -Depth 4)
}
} catch {
Write-Host "API Call Failed"
Write-Host $_.Exception.Message
}
}
# Main script execution
Write-Host "Starting Token Acquisition"
Get-InitialTokens
Ensure-ValidToken
Call-API
# Set up headers for authenticated requests
$headers = @{
'Authorization' = "Bearer $global:ACCESS_TOKEN"
'Content-Type' = 'application/json'
}
# Process each secret ID
$secretIds = $secretIdArray -split ","
foreach ($secretId in $secretIds) {
try {
# Get Secret
$getSecretUrl = "$apiUrl/secrets/$secretId"
$secret = Invoke-RestMethod -Uri $getSecretUrl -Method Get -Headers $headers
$secretName = $secret.name
Write-Debug "Updating Secret: $secretName"
# Find password fields and update them (matching original SOAP logic)
foreach ($item in $secret.items) {
if ($item.isPassword -eq $true) {
$item.itemValue = $newpassword
}
}
# Update Secret - pass the entire modified secret object
$updateSecretUrl = "$apiUrl/secrets/$secretId"
$updateBody = $secret | ConvertTo-Json -Depth 5
$updateResponse = Invoke-RestMethod -Uri $updateSecretUrl -Method Put -Headers
$headers -Body $updateBody
Write-Debug "Updated Secret: $secretName"
} catch {
Write-Debug "Error processing secret ID $secretId`: $($_.Exception.Message)"
continue
}
}