Overview
In order to manually backup the NPS-AM PostgreSQL database we must first change the password of the steathbits user setup by default at installation.
Description
I put together this document on steps to change the steathbits user password so the native PostgreSQL tools can then be used to backup the NPS-AM database. This covers changing the password for a single server and an HA pair of servers.
Important – If using a two server PostgreSQL pair you must make both databases active using the HA tool on the secondary to failover prior to starting this process
Step 1 – Primary Server
- Log into your NPS-AM server and navigate to the following path on your application drive (replace x with the appropriate drive letter)
x:\Program Files\Stealthbits\PAM\DatabaseConfig\DatabaseConfigUI
- Right click on NPS.DbCfg.exe and run as administrator.
This will launch the Netwrix Privilege Secure Database Configuration Tool:
- Select the radio button to change password and fill in your new password in the empty box.
Then select “Change Password” then “Test” and finally “Save”. If you have a single PostgreSQL server, the password update is complete. (see below to test connection and then update your secondary
server).
Verify New Password
To verify the change, you can close and reopen the Database Configuration Tool and replace the obfuscated password with your new password and select “Test” to verify your ability to connect to the database.
You should then see a message that your connection was successful.
Step 2 – HA Pair Secondary Server
After failover prior to starting the password change process on the primary server, both server databases become active and writable. On the secondary server we must follow the above process using the Database Configuration tool to change the password to the same value we did on the primary server, save, and test. Finally we can re-establish the HA pair.
Additional files
And a Powershell script that will setup a Scheduled Task to backup the database nightly at 2am.
# =============================================================================
# Netwrix Privilege Secure (NPS-AM) Postgresql backup scheduled task creation
#
# Purpose: Creates a ps1 and scheduled task to backup PG database using system account
# Author: Adam S. and Sonnet 4
# Date: May 29, 2025
# =============================================================================
## If the database password is entered incorrectly this can be run again to re-do the encrypted password file and any other settings
# you will be prompted for backup location and database password
# The scheduled task is set to run nightly at 2am
# Change pathing to change saved location of pw file and scheduled task script created
# Change pathing to change saved location of pw file and scheduled task script created
param(
[string]$ScriptPath = "C:\Scripts\PAM-Backup.ps1",
[string]$PasswordFile = "C:\Scripts\backup.key"
)
Write-Host "=== PAM DATABASE BACKUP - SETUP WIZARD ===" -ForegroundColor Green
Write-Host "This script will configure automated daily backups of your PAM database." -ForegroundColor Cyan
Write-Host ""
# Check if we're running as administrator
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Error "This script must be run as Administrator to configure SYSTEM account access."
Write-Host "Please right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Red
exit 1
}
Write-Host "Running as Administrator - good!" -ForegroundColor Green
Write-Host ""
# Get available drives
Write-Host "Available drives on this system:" -ForegroundColor Yellow
$AvailableDrives = Get-WmiObject -Class Win32_LogicalDisk | Where-Object {$_.DriveType -eq 3} | Select-Object -ExpandProperty DeviceID
foreach ($Drive in $AvailableDrives) {
Write-Host " $Drive" -ForegroundColor Cyan
}
Write-Host ""
# Prompt for drive letter
$DriveLetter = Read-Host "Enter the drive letter where Netwrix Privilege Secure is installed (e.g., C, D)"
$DriveLetter = $DriveLetter.TrimEnd(':').ToUpper()
# Construct PostgreSQL path
$PostgreSQLPath = "${DriveLetter}:\Program Files\Stealthbits\PostgreSQL16\bin"
# Validate PostgreSQL path
if (!(Test-Path "$PostgreSQLPath\pg_dump.exe")) {
Write-Error "pg_dump.exe not found in: $PostgreSQLPath"
Write-Host "Please verify that Netwrix Privilege Secure is installed on drive $DriveLetter" -ForegroundColor Red
exit 1
}
Write-Host "Found PostgreSQL at: $PostgreSQLPath" -ForegroundColor Green
Write-Host ""
# Prompt for backup location
Write-Host "Backup Configuration:" -ForegroundColor Yellow
$DefaultBackupPath = "C:\Backups\PAM"
$BackupPath = Read-Host "Enter backup directory path [$DefaultBackupPath]"
if ([string]::IsNullOrWhiteSpace($BackupPath)) {
$BackupPath = $DefaultBackupPath
}
Write-Host ""
Write-Host "Configuration Summary:" -ForegroundColor Cyan
Write-Host "PostgreSQL Path: $PostgreSQLPath"
Write-Host "Backup Path: $BackupPath"
Write-Host "Script Path: $ScriptPath"
Write-Host "Password File: $PasswordFile"
Write-Host ""
$Confirm = Read-Host "Continue with setup? (y/n)"
if ($Confirm -ne 'y' -and $Confirm -ne 'Y') {
Write-Host "Setup cancelled." -ForegroundColor Yellow
exit 0
}
Write-Host ""
Write-Host "Creating directories and files..." -ForegroundColor Yellow
# Create directories
$ScriptDir = Split-Path $ScriptPath
$PasswordDir = Split-Path $PasswordFile
if (!(Test-Path $ScriptDir)) {
New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null
Write-Host "Created: $ScriptDir" -ForegroundColor Green
}
if (!(Test-Path $PasswordDir)) {
New-Item -ItemType Directory -Path $PasswordDir -Force | Out-Null
Write-Host "Created: $PasswordDir" -ForegroundColor Green
}
if (!(Test-Path $BackupPath)) {
New-Item -ItemType Directory -Path $BackupPath -Force | Out-Null
Write-Host "Created: $BackupPath" -ForegroundColor Green
}
# Step 1: Create encrypted password file
Write-Host ""
Write-Host "Step 1: Setting up encrypted password..." -ForegroundColor Yellow
# Get the password securely (hidden input)
$SecurePassword = Read-Host "Enter password for stealthbits user" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)
$PlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
# Load required assembly for ProtectedData
Add-Type -AssemblyName System.Security
# Use machine-level encryption instead of user-level
$EncryptedBytes = [System.Security.Cryptography.ProtectedData]::Protect(
[System.Text.Encoding]::UTF8.GetBytes($PlainPassword),
$null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
# Convert to Base64 and save
$Base64 = [Convert]::ToBase64String($EncryptedBytes)
$Base64 | Out-File $PasswordFile -Encoding UTF8
Write-Host "Password encrypted and saved to: $PasswordFile" -ForegroundColor Green
# Step 2: Create the backup script file
Write-Host ""
Write-Host "Step 2: Creating backup script..." -ForegroundColor Yellow
$BackupScript = @"
param(
[string]`$BackupPath = "$BackupPath",
[string]`$PasswordFile = "$PasswordFile",
[string]`$PostgreSQLPath = "$PostgreSQLPath"
)
# Force create backup directory first thing - critical when running as SYSTEM
try {
if (!(Test-Path `$BackupPath)) {
New-Item -ItemType Directory -Path `$BackupPath -Force | Out-Null
}
} catch {
# If we can't even create the backup directory, we're in trouble
Write-Host "CRITICAL ERROR: Cannot create backup directory `$BackupPath - `$(`$_.Exception.Message)"
exit 1
}
# Enable logging with immediate file creation
`$LogFile = Join-Path `$BackupPath "PAM_Backup_Log_`$(Get-Date -Format 'yyyyMMdd').txt"
function Write-Log {
param([string]`$Message)
`$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
`$LogEntry = "`$Timestamp - `$Message"
# Always write to console
Write-Host `$LogEntry
# Try to write to log file, but don't fail if we can't
try {
Add-Content -Path `$LogFile -Value `$LogEntry -ErrorAction Stop
} catch {
Write-Host "Warning: Could not write to log file - `$(`$_.Exception.Message)"
}
}
# Create a startup marker to prove the script is running
try {
`$StartupMarker = Join-Path `$BackupPath "BACKUP_STARTED_`$(Get-Date -Format 'yyyyMMdd_HHmmss').marker"
"Backup process started" | Out-File `$StartupMarker -ErrorAction Stop
Write-Log "Created startup marker: `$StartupMarker"
} catch {
Write-Log "Warning: Could not create startup marker - `$(`$_.Exception.Message)"
}
try {
Write-Log "=== PAM DATABASE BACKUP STARTING ==="
Write-Log "Script version: 3.0 (SYSTEM-compatible with assembly loading)"
Write-Log "Running as user: `$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Log "Current working directory: `$PWD"
Write-Log "PowerShell version: `$(`$PSVersionTable.PSVersion)"
Write-Log "Backup path: `$BackupPath"
Write-Log "Password file: `$PasswordFile"
Write-Log "PostgreSQL path: `$PostgreSQLPath"
# Test all paths before starting
Write-Log "=== PRE-FLIGHT CHECKS ==="
Write-Log "Checking backup directory: `$BackupPath"
if (Test-Path `$BackupPath) {
`$BackupDirPerms = (Get-Acl `$BackupPath).Access | Where-Object {`$_.IdentityReference -like "*SYSTEM*"}
if (`$BackupDirPerms) {
Write-Log "✓ SYSTEM has access to backup directory"
} else {
Write-Log "⚠ SYSTEM permissions not explicitly set on backup directory"
}
} else {
throw "Backup directory does not exist: `$BackupPath"
}
Write-Log "Checking PostgreSQL path: `$PostgreSQLPath"
if (!(Test-Path "`$PostgreSQLPath\pg_dump.exe")) {
throw "pg_dump.exe not found in: `$PostgreSQLPath"
}
Write-Log "✓ PostgreSQL found successfully"
Write-Log "Checking password file: `$PasswordFile"
if (!(Test-Path `$PasswordFile)) {
throw "Password file not found: `$PasswordFile"
}
Write-Log "✓ Password file exists"
# Test password decryption before changing directories
Write-Log "=== PASSWORD DECRYPTION TEST ==="
try {
# Load required assembly for ProtectedData
Write-Log "Loading System.Security assembly..."
Add-Type -AssemblyName System.Security
`$Base64 = Get-Content `$PasswordFile -Raw
`$Base64 = `$Base64.Trim()
if ([string]::IsNullOrWhiteSpace(`$Base64)) {
throw "Password file is empty"
}
`$EncryptedBytes = [Convert]::FromBase64String(`$Base64)
`$DecryptedBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(
`$EncryptedBytes,
`$null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
`$PlainPassword = [System.Text.Encoding]::UTF8.GetString(`$DecryptedBytes)
if ([string]::IsNullOrWhiteSpace(`$PlainPassword)) {
throw "Decrypted password is empty"
}
Write-Log "✓ Password decrypted successfully (length: `$(`$PlainPassword.Length) characters)"
} catch {
Write-Log "✗ Password decryption failed: `$(`$_.Exception.Message)"
throw "Password decryption failed"
}
# Change to PostgreSQL directory
Write-Log "=== CHANGING TO POSTGRESQL DIRECTORY ==="
`$OriginalLocation = Get-Location
Set-Location `$PostgreSQLPath
Write-Log "Changed to PostgreSQL directory: `$(Get-Location)"
# Generate backup filename
`$BackupFile = Join-Path `$BackupPath "PAM_Backup_`$(Get-Date -Format 'yyyyMMdd_HHmmss').sql"
Write-Log "Backup file will be: `$BackupFile"
# Set password environment variable
`$env:PGPASSWORD = `$PlainPassword
Write-Log "Set PGPASSWORD environment variable"
# Execute pg_dump
Write-Log "=== EXECUTING PG_DUMP ==="
`$StartTime = Get-Date
Write-Log "Starting pg_dump at: `$StartTime"
# Use proper argument array for Start-Process
`$Arguments = @("-h", "localhost", "-U", "stealthbits", "-d", "PAM", "-f", `$BackupFile)
Write-Log "pg_dump arguments: `$(`$Arguments -join ' ')"
`$Process = Start-Process -FilePath ".\pg_dump.exe" -ArgumentList `$Arguments -Wait -PassThru -NoNewWindow
`$EndTime = Get-Date
`$Duration = `$EndTime - `$StartTime
Write-Log "pg_dump completed in `$(`$Duration.TotalMinutes.ToString('F2')) minutes with exit code: `$(`$Process.ExitCode)"
# Check results
if (`$Process.ExitCode -eq 0) {
if (Test-Path `$BackupFile) {
`$FileSize = [math]::Round((Get-Item `$BackupFile).Length / 1MB, 2)
Write-Log "✓ Database backup completed successfully!"
Write-Log "✓ Backup file: `$BackupFile"
Write-Log "✓ File size: `$FileSize MB"
# Quick validation - check if file has content
if (`$FileSize -gt 0) {
Write-Log "✓ Backup file size validation passed"
} else {
Write-Log "⚠ Warning: Backup file is 0 MB"
}
} else {
throw "pg_dump reported success but backup file was not created: `$BackupFile"
}
} else {
throw "pg_dump failed with exit code: `$(`$Process.ExitCode)"
}
# Cleanup old backups
Write-Log "=== CLEANUP OLD BACKUPS ==="
`$OldBackups = Get-ChildItem `$BackupPath -Filter "PAM_Backup_*.sql" | Where-Object {`$_.CreationTime -lt (Get-Date).AddDays(-7)}
if (`$OldBackups) {
foreach (`$OldBackup in `$OldBackups) {
Remove-Item `$OldBackup.FullName -Force
Write-Log "Deleted old backup: `$(`$OldBackup.Name)"
}
Write-Log "✓ Cleaned up `$(`$OldBackups.Count) old backup(s)"
} else {
Write-Log "No old backups to clean up"
}
Write-Log "=== BACKUP PROCESS COMPLETED SUCCESSFULLY ==="
} catch {
Write-Log "=== ERROR: BACKUP FAILED ==="
Write-Log "Error message: `$(`$_.Exception.Message)"
Write-Log "Stack trace: `$(`$_.ScriptStackTrace)"
Write-Log "Error category: `$(`$_.CategoryInfo.Category)"
Write-Log "Error at line: `$(`$_.InvocationInfo.ScriptLineNumber)"
# Create an error marker file for debugging
try {
`$ErrorMarker = Join-Path `$BackupPath "BACKUP_ERROR_`$(Get-Date -Format 'yyyyMMdd_HHmmss').marker"
`$_.Exception.Message | Out-File `$ErrorMarker
Write-Log "Created error marker: `$ErrorMarker"
} catch {
Write-Log "Could not create error marker"
}
exit 1
} finally {
# Always cleanup, even if an error occurs
Write-Log "=== CLEANUP ==="
# Clear password from memory
`$env:PGPASSWORD = `$null
if (`$PlainPassword) {
`$PlainPassword = `$null
Write-Log "Cleared password from memory"
}
# Return to original directory
if (`$OriginalLocation) {
Set-Location `$OriginalLocation
Write-Log "Returned to original directory: `$(Get-Location)"
}
# Force garbage collection
[System.GC]::Collect()
Write-Log "Performed garbage collection"
Write-Log "=== SCRIPT EXECUTION COMPLETED ==="
}
"@
$BackupScript | Out-File $ScriptPath -Encoding UTF8
Write-Host "Backup script created: $ScriptPath" -ForegroundColor Green
# Step 3: Create scheduled task
Write-Host ""
Write-Host "Step 3: Creating scheduled task..." -ForegroundColor Yellow
$TaskName = "PAM Database Backup"
$TaskDescription = "Daily backup of PAM database for Netwrix Privilege Secure"
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host "Removed existing scheduled task" -ForegroundColor Yellow
}
# Prompt for account selection
Write-Host ""
Write-Host "Choose how the scheduled task should run:" -ForegroundColor Cyan
Write-Host "1. SYSTEM account (recommended - runs whether logged in or not)"
Write-Host "2. Current user account with password (runs whether logged in or not)"
Write-Host "3. Current user account without password (only runs when logged in)"
Write-Host ""
$AccountChoice = Read-Host "Enter choice (1-3) [1]"
if ([string]::IsNullOrWhiteSpace($AccountChoice)) {
$AccountChoice = "1"
}
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-ExecutionPolicy Bypass -File `"$ScriptPath`""
$Trigger = New-ScheduledTaskTrigger -Daily -At "2:00AM"
$Settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5)
switch ($AccountChoice) {
"1" {
# Run as SYSTEM - most reliable for unattended operation
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$RegisterParams = @{
TaskName = $TaskName
Description = $TaskDescription
Action = $Action
Trigger = $Trigger
Settings = $Settings
Principal = $Principal
}
Write-Host "Configuring task to run as SYSTEM account..." -ForegroundColor Green
}
"2" {
# Run as current user with password
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$UserPassword = Read-Host "Enter password for user $CurrentUser" -AsSecureString
$UserCred = New-Object System.Management.Automation.PSCredential($CurrentUser, $UserPassword)
$RegisterParams = @{
TaskName = $TaskName
Description = $TaskDescription
Action = $Action
Trigger = $Trigger
Settings = $Settings
User = $UserCred.UserName
Password = $UserCred.GetNetworkCredential().Password
RunLevel = "Highest"
}
Write-Host "Configuring task to run as $CurrentUser with stored password..." -ForegroundColor Green
}
"3" {
# Run as current user without password (original behavior)
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$Principal = New-ScheduledTaskPrincipal -UserId $CurrentUser -LogonType ServiceAccount -RunLevel Highest
$RegisterParams = @{
TaskName = $TaskName
Description = $TaskDescription
Action = $Action
Trigger = $Trigger
Settings = $Settings
Principal = $Principal
}
Write-Host "Configuring task to run as $CurrentUser (only when logged in)..." -ForegroundColor Yellow
}
}
Register-ScheduledTask @RegisterParams
Write-Host ""
Write-Host "=== SETUP COMPLETED SUCCESSFULLY! ===" -ForegroundColor Green
Write-Host "Scheduled task '$TaskName' created successfully!" -ForegroundColor Green
if ($AccountChoice -eq "1") {
Write-Host "Task will run daily at 2:00 AM as SYSTEM (whether logged in or not)" -ForegroundColor Green
} elseif ($AccountChoice -eq "2") {
Write-Host "Task will run daily at 2:00 AM as $CurrentUser (whether logged in or not)" -ForegroundColor Green
} else {
Write-Host "Task will run daily at 2:00 AM as $CurrentUser (only when logged in)" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Configuration Details:" -ForegroundColor Cyan
Write-Host " PostgreSQL Path: $PostgreSQLPath"
Write-Host " Backup location: $BackupPath"
Write-Host " Password file: $PasswordFile"
Write-Host " Backup script: $ScriptPath"
Write-Host ""
Write-Host "To manually test the backup now:" -ForegroundColor Yellow
Write-Host " Start-ScheduledTask -TaskName '$TaskName'"
Write-Host ""
Write-Host "Or test the script directly:" -ForegroundColor Yellow
Write-Host " & '$ScriptPath'"
Write-Host ""
Write-Host "Setup complete. You can now close this window." -ForegroundColor Green
# Clear the plain password from memory
$PlainPassword = $null
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
[System.GC]::Collect()