Product: PowerShell Universal
Version: 5.6.13
I had been running PSU in my prod environment as the local SYSTEM account, and my instance connects to an external SQL database for state management. However, as I was building my dev server, I was instructed by the database team NOT to use a local SYSTEM account to access the database, so I am being forced to use a service account to run PSU. This would be fine, except it is now creating a lot of problems. I am getting a 404 error when sending requests to certain endpoints
The specific error I am getting is:
Invalid configuration: endpoints.ps1 Failed to login user (1326). System.ComponentModel.Win32Exception (1326): The user name or password is incorrect.
At first I thought this was because I didn’t yet assign the recommended GPO settings as defined here: Running as a Service Account | PowerShell Universal
But now I’ve done all that, and I’m concerned that it simply can’t authenticate a gMSA when running as a service account. Is this a limitation of PSU or is there something else I need to do?
I use PSU with with the local system account and it works great. However, you should to be able to use credentials on-demand. For SQL server, I highly recommend using dbatools. Install their module, provide the credentials. We have multiple domains. In many scenarios, its makes much better sense to use a local SQL account, and not a domain account. Want to be in Domain A, but update a SQL server in Domain B, far easier to use local SQL accounts.
We happen to use CyberArk to keep credentials secured in an API safe, but that is not required and there are plenty of options (goal is… no account info is actually on the PSU server).
You seem to want to run code without specifying credentials. Honestly, that’s insecure and considered a bad practice. The local system account should only have permissions to the local server. If compromised, only the local server is compromised. If that account can access other servers, its all broken. Its the BEST option to use a limited SYSTEM account.
Use a secrets vault or something like CyberArk to obtain credentials. The account running PSU shouldn’t have permissions to run anything but PSU. Its easy for almost everything… everything except Microsoft code where some of their PowerShell modules/code demands trust (I.e. must be in the same domain and the code run from a trusted domain computer… and it blocks using a different account) thus… you can’t elevate the code (have admin access) using a different/supplied credential account… just won’t let you… UNLESS… you use PSEXEC which is very complex but once you get it… works. That is something I cannot share, but its out there in the web. SQL server NEVER needs that btw. Any form. Re-write your code to use specified credentials, not the credentials running PSU.
I reviewed this code and nothing is proprietary; however it is an example of wanting to backup Windows Printers/Queues using different credentials than the account running the service, and how PSEXEC is complex (mainly how to supply arguments, credentials and other minor tweaks like the “2>&1” - look it up).
if ($null -ne $sharedPrinters) {
$BackUpFilePath = "D:\\PrinterBackups\\" + $Server.Name + "\_" + $todaysDate + ".PrinterExport"
$BackupLogFile = "D:\\PrinterBackups\\Logs\\" + $Server.Name + "\_" + $todaysDate + ".Log"
try {
$serverPath = "\\\\" + $Server.Name
$psexecArgs = @(
"\\\\localhost", # runs on management/actual server
"-accepteula",
"-u", $adUser, # use AD credential from CyberArk
"-p", $adPassword, # use password from CA
"-h", # elevated
$using:PrintBRMPath, # PrintBrm.exe path
"-B", # use backup mode
"-S", $serverPath, # target server (with "\\\\" prefix)
"-F", $BackUpFilePath # output backup file
)
$psexecOutput = & $using:psexecPath @psexecArgs 2>&1
$psexecOutput | Out-File -FilePath $BackupLogFile -Force -Encoding ascii | Out-Null
} catch {
Write-Information -InformationAction Continue -MessageData "\[$((Get-Date -Format 'g'))\] Error PrintBrm for Backup task on $($Server.Name): $($\_.Exception.Message)"
($Server.Name + "\_PrintBrmFailed") | Out-File -FilePath "D:\\PrinterBackups\\Logs\\PrintBrm_Failed.Log" -Force -Encoding ascii -append | Out-Null
}
}
For obvious reasons, I can’t give the code to the functions used, but this is typical of getting an account from CyberArk. The functions are easy to create if you use/understand CyberArk. It could work for any secure source to retrieve credentials.
$adAcct = Get-DomainAccount # Based on server running PSU, get the proper domain account to access domain servers
$adCred = Get-CAAccountCredential -AccountName $adAcct # Gets the actual account in the form of PSCredentials from a CyberArk safe
$domain = (Get-PSUEnvironmentInfo).ShortDomain #Gets the domain name based on the running PSU server
$adUser = $domain + "\\" + $adCred.UserName # Gets the credential username in DOMAIN\\Username format
$adPassword = $adCred.GetNetworkCredential().Password # Gets the plaintext password for PSEXEC which doesn’t handle SecureString
I 100% agree with you in that the local SYSTEM account should only have access to the local system. That said, I don’t see how PSU can run as the local system account and simultaneously read/write to the sql database as anything else?
Let me reiterate by saying I don’t have a problem with your proposed solution. I just have a problem with the fact that you didn’t explain HOW you implemented it… How is it supposed to explicitly use designated creds to authenticate to the database when, as far as I understand it, that is not how PSU is designed to work?
For scripting, any time access to a database is used, I DO explicitly authenticate, but for general application state access, PSU does not support using separate credentials.
I’m talking about:
PSU state database (internal)
- Used by PSU to store jobs, schedules, dashboards, auth state, etc.
- Connection created by the PSU service itself.
- Not “on-demand,” not per-script.
- Auth options are basically:
- Windows Integrated (service identity, i.e., SYSTEM →
DOMAIN\HOST$ on the network)
- SQL Authentication (username/password in connection string)
- Possibly certificate/other SQL-supported mechanisms depending on driver, but still “app config,” not per-execution.
Thanks
Okay, you are specifically referring to PSU’s repository database. We use SQL server along with an on-prem GitLab. In the example below, this PSU instance is in one domain and the SQL server its using is in another. Its using a specified SQL account. Obviously computer, domain, and user info changed for the example. This is the appsettings.json file:
{
“SystemLogPath”: “%ProgramData%/PowerShellUniversal/Logs/system.log”,
“SystemLogLevel”: “Debug”,
“Authentication”: {
“Windows”: {
“Enabled”: “true”
}
},
“Kestrel”: {
“RedirectToHttps”: “true”,
“Endpoints”: {
“HTTPS”: {
“Url”: “https://*:443”,
“Protocols”: “Httpl”,
“SslProtocols”: [“Tls12”, “Tls13”],
“Certificate”: {
“Subject”: “customUrl.domainA.local”,
“Store”: “My”,
“Location”: “LocalMachine”,
“AllowInvalid”: “true”
}
}
},
“Limits”: {
“MaxRequestHeadersTotalSize”: 132768
},
},
“Windows”: {
“Enabled”: “true”
},
“Plugins”: [
“SQL”
],
“Data”: {
“RepositoryPath”: “%ProgramData%\UniversalAutomation\Repository”,
“ConnectionString”: “Server=SQLSERVER.customdomainB.local\PRDESSPSUSQL1,1433;Database=PSConfigDevDb;User Id=;Password=;TrustServerCertificate=True;”,
“GitUserName”: “Private-Token”,
“GitPassword”: “”,
“Persistence” : {
“Schedule”: “Database”,
“ScheduleParameter”: “Database”
}
}, “NodeName”: “PSUCOMPUTERA”
}
That settings file allows PSU to access SQL server using a SQL local account, not a domain account. Its especially necessary when a PSU server is in one domain and the SQL server in another. There are techniques to encrypt the username and password, but that is something else entirely and what we use, we cannot share. Options can be found searching the web. Might be a good feature request: have appsettings.json file for PSU support encrypted connection strings and Git tokens.
1 Like
Hmm.. I had username and password specified with dummy examples surrounded by characters that apparently have it auto-removed. I’m sure you can tell where the SQL account and password go in the connection string as well as the GitLab token/password.
Yeah I’m not sure how you would put encrypted strings inside the json file. I’m trying to decode secure strings as env vars, but the appsettings.json seems to override that
Ah! I think I got it. I used a method involving setting the connection string as an environment variable for the PSU service itself, and just removed the connection string from the appsettings.json file. That seems to have done the trick! I appreciate the insight
Awesome. FYI: I submitted a request for PSU to support encrypted connections strings and tokens in the appsettings.json file. Hopefully, they’ll add it to a future release soon.
Just to provide additional information that might help anyone with the same problem. This is what I ended up doing. I used two scripts.
One to create a DPAPI blob in a given registry path:
# SEAL Script
# --- CONFIG ---
$RegPath = 'HKLM:\SOFTWARE\your\path\psu'
$ValueName = 'SqlPasswordDpapi'
# Create the registry key if needed
if (-not (Test-Path $RegPath)) {
New-Item -Path $RegPath -Force | Out-Null
}
# Prompt for the SQL password (input is hidden)
$plain = Read-Host -Prompt 'Enter the SQL password to store (will be DPAPI-protected)' -AsSecureString
# Convert SecureString -> plaintext in memory only (briefly)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($plain)
try {
$plaintextPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
Add-Type -AssemblyName System.Security
# DPAPI-protect the bytes (LocalMachine scope means decryptable on this machine)
$bytes = [Text.Encoding]::UTF8.GetBytes($plaintextPassword)
$protected = [Security.Cryptography.ProtectedData]::Protect(
$bytes,
$null,
[Security.Cryptography.DataProtectionScope]::LocalMachine
)
# Store as Base64 in registry
$blob = [Convert]::ToBase64String($protected)
New-ItemProperty -Path $RegPath -Name $ValueName -Value $blob -PropertyType String -Force | Out-Null
Write-Host "Stored DPAPI-protected password in $RegPath ($ValueName)." -ForegroundColor Green
I created a second wrapper script for decrypting the DPAPI blob and starting up the service at boot
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Security
function Write-PSUStartupLog {
param(
[Parameter(Mandatory)][string]$Message,
[ValidateSet('Information','Warning','Error')][string]$Level = 'Information',
[int]$EventId = 1000
)
Write-EventLog -LogName 'Application' -Source 'PSU-Startup' -EntryType $Level -EventId $EventId -Message $Message
}
try {
# --- CONFIG ---
$RegPath = 'HKLM:\SOFTWARE\your\path\psu'
$ValueName = 'SqlPasswordDpapi'
$SqlServer = 'SERVERNAME' # e.g. sql01.company.local,1433 OR sql01\INSTANCE
$Database = 'DATABASENAME'
$SqlUser = 'SQLLOGIN'
$ServiceName = 'PowerShellUniversal'
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName"
Write-PSUStartupLog -Message "PSU wrapper starting. Preparing connection string for $SqlServer/$Database." -EventId 1000
# Read blob
$blob = (Get-ItemProperty -Path $RegPath -Name $ValueName).$ValueName
if (-not $blob) { throw "DPAPI blob missing at $RegPath ($ValueName)" }
# Decode + DPAPI-unprotect
$protectedBytes = [Convert]::FromBase64String($blob)
$bytes = [Security.Cryptography.ProtectedData]::Unprotect(
$protectedBytes,
$null,
[Security.Cryptography.DataProtectionScope]::LocalMachine
)
if (-not $bytes) { throw "DPAPI Unprotect returned null bytes." }
$password = [Text.Encoding]::UTF8.GetString($bytes)
# Build connection string
$conn = "Server=$SqlServer;Database=$Database;User ID=$SqlUser;Password=$password;Encrypt=True;TrustServerCertificate=True;"
# Optional: quick SQL reachability check (avoids boot-time race)
if (-not (Test-NetConnection -ComputerName ($SqlServer -replace '\\.*','' -replace ',.*','') -Port 1433 -InformationLevel Quiet)) {
Write-PSUStartupLog -Message "SQL not reachable yet (1433). Will still attempt PSU start." -Level Warning -EventId 3001
}
# Read existing per-service env block (REG_MULTI_SZ)
$existing = @()
try {
$existing = (Get-ItemProperty -Path $svcKey -Name Environment -ErrorAction Stop).Environment
if ($existing -is [string]) { $existing = @($existing) }
} catch {
$existing = @()
}
$existing = $existing | Where-Object { $_ -notmatch '^Data__ConnectionString=' }
$newEnv = @($existing + "Data__ConnectionString=$conn")
New-ItemProperty -Path $svcKey -Name Environment -PropertyType MultiString -Value $newEnv -Force | Out-Null
Write-PSUStartupLog -Message "Injected Data__ConnectionString into per-service Environment for $ServiceName." -EventId 1001
# Start PSU
if ((Get-Service -Name $ServiceName).Status -ne 'Stopped') {
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
Start-Sleep 2
}
Start-Service -Name $ServiceName
Start-Sleep 5
$svc = Get-Service -Name $ServiceName
if ($svc.Status -ne 'Running') {
Write-PSUStartupLog -Message "PSU service failed to reach Running state. Current state: $($svc.Status)" -Level Error -EventId 2002
exit 1
}
Write-PSUStartupLog -Message 'PSU service is running.' -Level Information -EventId 1002
}
catch {
Write-PSUStartupLog -Message "PSU wrapper failed: $($_.Exception.Message)" -Level Error -EventId 2001
exit 1
}
finally {
Remove-Variable password -ErrorAction SilentlyContinue
}
Once you can confirm that the script starts PSU the way you intend, you can remove the SQL string from your appsettings.json entirely. The script also puts logs in the event viewer under Windows Logs > Application and filter the source by “PSU-Startup”
Finally, I just set up a scheduled task in Task Scheduler to run this as SYSTEM at startup. Hopefully that will help anyone who struggled with the same problem!