Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

RDP Privileged Access : Periscope Peek


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:

  1. ASP.NET Web Application - Provides the user interface and orchestrates security checks
  2. PowerShell Script - Handles Active Directory GPO operations using the GroupPolicy module
  3. 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:

  1. Used the Service account in the Delegate Wizard
  2. 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 improvements
  • Get-GPInheritance is much faster than Invoke-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
Previous Post Next Post

نموذج الاتصال