I needed to optimize the web-based management console for controlling Privileged Access Management (PAM) RDP enforcement policies in an Active Directory environment - this was mainly covered in the post here
This post is more about the overview for the website logic and the backend script that actually checks/manipulates group policy links and objects.
Architecture Overview
The solution consists of three main components:
- ASP.NET Web Application - Provides the user interface and orchestrates security checks
- PowerShell Script - Handles Active Directory GPO operations using the GroupPolicy module
- Privileged Access Service Monitoring - Ensures the PAM system is offline before allowing policy overrides
Key Technical Challenges and Solutions
Challenge 1: PowerShell Performance Optimization
Initially, the PowerShell script took 3+ minutes to execute due to Invoke-GPUpdate
forcing Group Policy refreshes. This made the web interface unusable.
Original Slow Approach:
# This was taking 3+ minutes
Invoke-GPUpdate -Computer $env:COMPUTERNAME -Force
Optimized Solution:
# Fast status checking using Get-GPInheritance
$inheritance = Get-GPInheritance -Target $Config.TargetOU -ErrorAction Stop
$linkedPolicies = $inheritance.GpoLinks | Where-Object { $_.Enabled -eq $true }
Web Application PowerShell Execution:
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = string.Format("-ExecutionPolicy Bypass -NoProfile -NoLogo -File {0}", scriptCommand),
// -NoProfile and -NoLogo significantly reduce startup time
};
This reduced execution time from 3+ minutes to 2-5 seconds.
Challenge 2: Multi-Service Availability Checking
The system needed to verify that multiple Beyond Trust service instances were offline before allowing policy overrides.
Implementation:
private bool CheckBeyondTrustAvailability()
{
ServiceStatusMessages = new List<string>();
bool anyServiceOnline = false;
foreach (string url in BeyondTrustUrls)
{
bool isThisServiceOnline = CheckSingleService(url);
if (isThisServiceOnline)
{
anyServiceOnline = true;
ServiceStatusMessages.Add(url + ": ONLINE");
}
else
{
ServiceStatusMessages.Add(url + ": OFFLINE");
}
}
// Policy controls only available when ALL services are offline
return anyServiceOnline;
}
Corporate Proxy Handling:
private bool CheckSingleService(string url)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
// Configure corporate proxy
string proxyUrl = ConfigurationManager.AppSettings["ProxyUrl"];
if (!string.IsNullOrEmpty(proxyUrl))
{
WebProxy proxy = new WebProxy(proxyUrl);
proxy.UseDefaultCredentials = true;
request.Proxy = proxy;
}
// Only HTTP 200 = service online, everything else = offline
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
return response.StatusCode == HttpStatusCode.OK;
}
}
Challenge 3: Status Mapping Between PowerShell and Web Interface
The PowerShell script returned different field names than the web application expected, causing status display issues.
PowerShell Output:
{
"OverallStatus": "ACTIVE",
"Success": true,
"StatusSummary": "PAM Admin Policy is linked and enabled"
}
Web Application Mapping:
public class ScriptResult
{
public bool Success { get; set; }
public string CurrentState { get; set; }
public string OverallStatus { get; set; } // Added to match PS output
public string Message { get; set; }
}
// Map PowerShell statuses to web interface statuses
CurrentPolicyStatus = !string.IsNullOrEmpty(scriptResult.CurrentState) ?
scriptResult.CurrentState :
(scriptResult.OverallStatus == "ACTIVE" ? "PAM_ENABLED" :
scriptResult.OverallStatus == "DEGRADED" ? "DEGRADED" : "UNKNOWN");
Challenge 4: Least Privilege Service Account Configuration
The application required specific Active Directory permissions without granting excessive privileges.
Delegated Permissions Approach:
- Used the Service account in the Delegate Wizard
- Delegated "Link Group Policy Objects" permission to the target OU only for that service account
This approach follows the principle of least privilege while providing necessary functionality.
Challenge 5: Backend Script for GPO Operations
The web interface needed a reliable backend script to handle Group Policy operations across multiple organizational units. The requirements were specific: monitor and manage the "PAM Admin Policy" across both the PAM Infrastructure and Domain Controllers OUs, with granular status reporting.
Configuration Updates
I updated the configuration section to handle multiple target OUs and simplified the policy structure:
# Configuration Variables
$Config = @{
# Target OUs for linking policies
TargetOUs = @(
"OU=PAM Infrastructure,DC=stwater,DC=intra",
"OU=Domain Controllers,DC=stwater,DC=intra"
)
# Group Policy for PAM RDP Enforcement
PAMPolicy = "PAM Admin Policy"
}
The log path was also updated to match the quarantine directory structure:
[string]$LogPath = "C:\Quarantine\RDPEnforce\PAM-RDP-Management.log"
Multi-OU Status Logic
The core challenge was implementing intelligent status reporting. The script needed to distinguish between three states:
- ACTIVE: Policy linked and enabled on ALL target OUs
- DEGRADED: Policy enabled on SOME but not all target OUs
- DISABLED: Policy not enabled on any target OUs
I implemented individual OU checking first:
function Get-OUPolicyStatus {
param([string]$OU)
try {
$inheritance = Get-GPInheritance -Target $OU -ErrorAction Stop
$linkedPolicies = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $Config.PAMPolicy }
if ($linkedPolicies) {
$policy = $linkedPolicies[0]
if ($policy.Enabled -eq $true) {
return "ENABLED"
} else {
return "LINKED_DISABLED"
}
} else {
return "NOT_LINKED"
}
}
catch {
return "ERROR"
}
}
Then aggregated results across all OUs:
function Get-CurrentPolicyState {
try {
$ouStatuses = @{}
$enabledCount = 0
$errorCount = 0
foreach ($ou in $Config.TargetOUs) {
$status = Get-OUPolicyStatus -OU $ou
$ouStatuses[$ou] = $status
if ($status -eq "ENABLED") {
$enabledCount++
} elseif ($status -eq "ERROR") {
$errorCount++
}
}
# Determine overall status
if ($errorCount -gt 0) {
return @{
OverallStatus = "ERROR"
OUStatuses = $ouStatuses
Summary = "Errors occurred while checking policy status"
}
}
elseif ($enabledCount -eq $Config.TargetOUs.Count) {
return @{
OverallStatus = "ACTIVE"
OUStatuses = $ouStatuses
Summary = "PAM Admin Policy is linked and enabled on all $($Config.TargetOUs.Count) target OUs"
}
}
elseif ($enabledCount -gt 0) {
return @{
OverallStatus = "DEGRADED"
OUStatuses = $ouStatuses
Summary = "PAM Admin Policy is only enabled on $enabledCount of $($Config.TargetOUs.Count) target OUs"
}
}
else {
return @{
OverallStatus = "DISABLED"
OUStatuses = $ouStatuses
Summary = "PAM Admin Policy is not enabled on any target OUs"
}
}
}
catch {
return @{
OverallStatus = "ERROR"
OUStatuses = @{}
Summary = "Unexpected error: $($_.Exception.Message)"
}
}
}
Policy Management Functions
The enable and disable operations needed to work across both OUs reliably. For adding policy links:
function Add-PolicyLinks {
$allSuccess = $true
# First check if GPO exists
try {
$gpo = Get-GPO -Name $Config.PAMPolicy -ErrorAction Stop
}
catch {
Write-LogEntry "Group Policy Object not found: $($Config.PAMPolicy)" "ERROR"
return $false
}
foreach ($ou in $Config.TargetOUs) {
try {
$inheritance = Get-GPInheritance -Target $ou -ErrorAction Stop
$existingLink = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $Config.PAMPolicy }
if (!$existingLink) {
New-GPLink -Name $Config.PAMPolicy -Target $ou -LinkEnabled Yes -Enforced Yes
Write-LogEntry "Successfully added and enforced policy link to OU: $ou"
}
else {
Set-GPLink -Name $Config.PAMPolicy -Target $ou -LinkEnabled Yes -Enforced Yes
Write-LogEntry "Updated existing policy link for OU: $ou (enabled and enforced)"
}
}
catch {
Write-LogEntry "Failed to add policy link to OU ${ou}: $($_.Exception.Message)" "ERROR"
$allSuccess = $false
}
}
return $allSuccess
}
JSON Output Structure
The script returns structured data that the web interface can consume directly:
$result = @{
Success = $true
Message = "PAM RDP $Action operation completed successfully"
OverallStatus = $finalState.OverallStatus
StatusSummary = $finalState.Summary
OUDetails = $finalState.OUStatuses
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
TargetOUs = $Config.TargetOUs
PAMPolicy = $Config.PAMPolicy
}
Write-Output ($result | ConvertTo-Json -Depth 3)
The status command provides detailed breakdown of policy state per OU, enabling the web interface to display granular information about the PAM enforcement status across the infrastructure.
Web Interface Design
The interface provides clear visual feedback about system status:
Status Indicators:
- Green: PAM RDP Enforcement Active
- Red: PAM RDP Enforcement Inactive
- Orange: PAM RDP Enforcement Degraded
- Spinning: Loading current configuration
Security Controls:
- User authorization banner (red/green)
- Service availability status (multiple URLs)
- Confirmation dialogs for policy changes
- Audit logging for all actions
Configuration Management
Web.config Settings:
<appSettings>
<add key="BeyondTrustUrls" value="https://data.pamapp.local/console,https://data.pamapp.local/console" />
<add key="ProxyUrl" value="http://squid.bear.local:3129" />
<add key="AuthorizedUsers" value="bear\authorised.user,authorised.user" />
</appSettings>
Lessons Learned
Performance Optimization
- PowerShell module loading causes unavoidable first-run delays
-NoProfile
and-NoLogo
flags provide significant startup improvementsGet-GPInheritance
is much faster thanInvoke-GPUpdate
for status checking- PowerShell Core (pwsh.exe) doesn't support the GroupPolicy module
Security Considerations
- Multiple service checking prevents single points of failure
- Delegated permissions provide adequate access without Domain Admin rights
- Corporate proxy configurations require careful SSL/TLS handling
- Input validation and logging are essential for audit compliance
Error Handling
- Robust fallback parsing for both JSON and plain text responses
- Comprehensive logging for troubleshooting
- Clear user feedback for authorization and service availability issues