This comprehensive guide provides enterprise-level strategies for implementing Privileged Account Management (PAM) in Active Directory environments with modern security practices, automation, Just-In-Time access, and Zero Trust principles.
Overview
Privileged Account Management (PAM) is a critical security discipline that focuses on securing, controlling, and monitoring privileged accounts and access rights within an IT environment. Modern PAM implementations incorporate Zero Trust principles, Just-In-Time access, and comprehensive automation to minimize security risks while maintaining operational efficiency.
Prerequisites
Technical Requirements
- Windows Server 2019 or later (Windows Server 2022 recommended)
- Active Directory Domain Services with appropriate functional levels
- PowerShell 5.1 or later with ActiveDirectory module
- Microsoft Identity Manager (MIM) or equivalent PAM solution (optional)
- Azure AD Premium P2 (for hybrid scenarios with PIM)
- Certificate Authority for smart card authentication
Planning Requirements
- Privileged access risk assessment completed
- Administrative tier model designed
- Role-based access control (RBAC) matrix defined
- Emergency access procedures documented
- Compliance requirements identified
Security Requirements
- Multi-factor authentication (MFA) implementation
- Privileged Access Workstations (PAWs) deployment
- Network segmentation for administrative access
- Comprehensive logging and monitoring capabilities
- Incident response procedures for privileged access
Privileged Account Management Architecture
Tiered Administrative Model
Modern PAM implementations use a tiered model to minimize lateral movement and privilege escalation:
┌─────────────────────────────────────────┐
│ Tier 0 │
│ (Control Plane) │
│ • Domain Controllers │
│ • Forest Root Domain │
│ • Certificate Authorities │
│ • ADFS/Identity Providers │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ Tier 1 │
│ (Resource Plane) │
│ • Member Servers │
│ • File Servers │
│ • Application Servers │
│ • Database Servers │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ Tier 2 │
│ (User Plane) │
│ • User Workstations │
│ • VDI Infrastructure │
│ • End-User Applications │
│ • Client Devices │
└─────────────────────────────────────────┘
Core PAM Components
- Administrative Accounts: Separate accounts for different privilege levels
- Privileged Groups: Carefully managed high-privilege security groups
- Access Control: Time-bound and just-in-time access mechanisms
- Monitoring: Comprehensive auditing and threat detection
- Automation: Scripted provisioning and de-provisioning processes
Secure Organizational Unit Structure
PAM OU Design
# Create secure OU structure for privileged accounts
function New-PAMOUStructure {
param(
[string]$DomainDN = (Get-ADDomain).DistinguishedName,
[string]$BasePAMOU = "OU=Privileged Access Management,$DomainDN"
)
try {
# Create main PAM OU
if (-not (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$BasePAMOU'" -ErrorAction SilentlyContinue)) {
New-ADOrganizationalUnit -Name "Privileged Access Management" -Path $DomainDN -Description "Secure container for privileged accounts and groups"
Write-Host "Created main PAM OU: $BasePAMOU" -ForegroundColor Green
}
# Create tier-based sub-OUs
$TierOUs = @{
'Tier0' = 'Control Plane - Domain Controllers and Forest Infrastructure'
'Tier1' = 'Resource Plane - Server Infrastructure'
'Tier2' = 'User Plane - Workstation and End-User Resources'
'ServiceAccounts' = 'Service Accounts with Elevated Privileges'
'EmergencyAccess' = 'Break-Glass Emergency Access Accounts'
'Groups' = 'Privileged Security Groups'
'Workstations' = 'Privileged Access Workstations (PAWs)'
}
foreach ($OU in $TierOUs.Keys) {
$OUPath = "OU=$OU,$BasePAMOU"
if (-not (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$OUPath'" -ErrorAction SilentlyContinue)) {
New-ADOrganizationalUnit -Name $OU -Path $BasePAMOU -Description $TierOUs[$OU]
Write-Host "Created $OU OU: $OUPath" -ForegroundColor Green
}
}
# Configure OU security
Set-PAMOUSecurity -PAMOU $BasePAMOU
Write-Host "PAM OU structure created successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to create PAM OU structure: $($_.Exception.Message)"
}
}
# Configure security for PAM OUs
function Set-PAMOUSecurity {
param(
[Parameter(Mandatory)]
[string]$PAMOU
)
try {
# Block inheritance on main PAM OU
$ACL = Get-ACL -Path "AD:$PAMOU"
$ACL.SetAccessRuleProtection($true, $false) # Block inheritance, don't copy existing permissions
# Define secure ACL for PAM OU
$SecureACEs = @(
@{
Principal = 'Domain Admins'
Rights = 'FullControl'
AccessControlType = 'Allow'
InheritanceType = 'All'
},
@{
Principal = 'Enterprise Admins'
Rights = 'FullControl'
AccessControlType = 'Allow'
InheritanceType = 'All'
},
@{
Principal = 'SYSTEM'
Rights = 'FullControl'
AccessControlType = 'Allow'
InheritanceType = 'All'
},
@{
Principal = 'Enterprise Domain Controllers'
Rights = 'ReadProperty'
AccessControlType = 'Allow'
InheritanceType = 'Children'
}
)
# Apply secure ACEs
foreach ($ACE in $SecureACEs) {
$Principal = New-Object System.Security.Principal.SecurityIdentifier((Get-ADGroup $ACE.Principal).SID)
$AccessRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$Principal,
$ACE.Rights,
$ACE.AccessControlType,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)
$ACL.SetAccessRule($AccessRule)
}
Set-ACL -Path "AD:$PAMOU" -AclObject $ACL
# Configure auditing on PAM OU
Set-PAMOUAuditing -PAMOU $PAMOU
Write-Host "PAM OU security configured successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to configure PAM OU security: $($_.Exception.Message)"
}
}
# Configure comprehensive auditing for PAM OUs
function Set-PAMOUAuditing {
param(
[Parameter(Mandatory)]
[string]$PAMOU
)
try {
$AuditACL = Get-ACL -Path "AD:$PAMOU" -Audit
# Define audit rules for privileged access monitoring
$AuditEvents = @(
'WriteProperty',
'Delete',
'DeleteChild',
'WriteOwner',
'WriteDacl',
'CreateChild',
'ExtendedRight'
)
foreach ($Event in $AuditEvents) {
$AuditRule = New-Object System.DirectoryServices.ActiveDirectoryAuditRule(
[System.Security.Principal.SecurityIdentifier]::new('S-1-1-0'), # Everyone
[System.DirectoryServices.ActiveDirectoryRights]::$Event,
[System.Security.AccessControl.AuditFlags]::Success,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)
$AuditACL.SetAuditRule($AuditRule)
}
Set-ACL -Path "AD:$PAMOU" -AclObject $AuditACL
Write-Host "PAM OU auditing configured successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to configure PAM OU auditing: $($_.Exception.Message)"
}
}
Privileged Account Creation and Management
Administrative Account Lifecycle
# Create privileged administrative accounts with proper security settings
function New-PrivilegedAccount {
param(
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[ValidateSet('Tier0', 'Tier1', 'Tier2', 'ServiceAccount', 'EmergencyAccess')]
[string]$Tier,
[Parameter(Mandatory)]
[string]$UserPrincipalName,
[string]$Description,
[string]$Manager,
[datetime]$AccountExpiry = (Get-Date).AddYears(1),
[switch]$RequireSmartCard,
[string[]]$MemberOfGroups = @()
)
try {
# Determine OU based on tier
$DomainDN = (Get-ADDomain).DistinguishedName
$PAMOU = "OU=Privileged Access Management,$DomainDN"
$TierOU = "OU=$Tier,$PAMOU"
# Generate secure password
$SecurePassword = New-SecurePassword -Length 24 -IncludeSpecialCharacters
# Create privileged account with secure settings
$AccountParams = @{
Name = $Username
SamAccountName = $Username
UserPrincipalName = $UserPrincipalName
Path = $TierOU
Description = $Description
Manager = $Manager
AccountPassword = $SecurePassword
ChangePasswordAtLogon = $true
Enabled = $true
PasswordNeverExpires = $false
PasswordNotRequired = $false
SmartcardLogonRequired = $RequireSmartCard
AccountExpirationDate = $AccountExpiry
CannotChangePassword = $false
TrustedForDelegation = $false
AllowReversiblePasswordEncryption = $false
}
New-ADUser @AccountParams
# Set additional security attributes
Set-ADUser -Identity $Username -Replace @{
'msDS-User-Account-Control-Computed' = 0x0020 # NORMAL_ACCOUNT
'userAccountControl' = 0x0200 # NORMAL_ACCOUNT + DONT_EXPIRE_PASSWORD (will be managed by policy)
}
# Add to appropriate groups
foreach ($Group in $MemberOfGroups) {
try {
Add-ADGroupMember -Identity $Group -Members $Username
Write-Host "Added $Username to group $Group" -ForegroundColor Green
}
catch {
Write-Warning "Failed to add $Username to group $Group`: $($_.Exception.Message)"
}
}
# Log account creation
Write-EventLog -LogName 'Application' -Source 'PAM-Management' -EventId 1001 -EntryType Information -Message "Privileged account created: $Username (Tier: $Tier)"
Write-Host "Privileged account created successfully: $Username" -ForegroundColor Green
return @{
Username = $Username
Tier = $Tier
OU = $TierOU
Password = $SecurePassword
ExpiryDate = $AccountExpiry
}
}
catch {
Write-Error "Failed to create privileged account $Username`: $($_.Exception.Message)"
}
}
# Generate cryptographically secure passwords
function New-SecurePassword {
param(
[int]$Length = 24,
[switch]$IncludeSpecialCharacters
)
$CharacterSets = @(
'ABCDEFGHIJKLMNOPQRSTUVWXYZ', # Uppercase
'abcdefghijklmnopqrstuvwxyz', # Lowercase
'0123456789' # Numbers
)
if ($IncludeSpecialCharacters) {
$CharacterSets += '!@#$%^&*()_+-=[]{}|;:,.<>?'
}
$AllCharacters = $CharacterSets -join ''
$Random = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$Password = @()
# Ensure at least one character from each set
foreach ($Set in $CharacterSets) {
$Bytes = New-Object byte[] 1
$Random.GetBytes($Bytes)
$Password += $Set[$Bytes[0] % $Set.Length]
}
# Fill remaining length
for ($i = $CharacterSets.Count; $i -lt $Length; $i++) {
$Bytes = New-Object byte[] 1
$Random.GetBytes($Bytes)
$Password += $AllCharacters[$Bytes[0] % $AllCharacters.Length]
}
# Shuffle the password
$ShuffledPassword = ($Password | Sort-Object {Get-Random}) -join ''
$Random.Dispose()
return ConvertTo-SecureString -String $ShuffledPassword -AsPlainText -Force
}
# Automated privileged account lifecycle management
function Invoke-PrivilegedAccountLifecycle {
param(
[int]$ExpiryWarningDays = 30,
[int]$InactiveAccountDays = 90,
[string]$ReportPath = "C:\Reports\PAM_Lifecycle_$(Get-Date -Format 'yyyyMMdd_HHmmss').html",
[string]$AlertEmail = $null,
[string]$SMTPServer = $null
)
try {
$DomainDN = (Get-ADDomain).DistinguishedName
$PAMOU = "OU=Privileged Access Management,$DomainDN"
# Get all privileged accounts
$PrivilegedAccounts = Get-ADUser -SearchBase $PAMOU -Filter * -Properties LastLogonDate, AccountExpirationDate, PasswordLastSet, PasswordExpired, LockedOut, Enabled
$LifecycleResults = @{
ExpiringAccounts = @()
InactiveAccounts = @()
PasswordExpired = @()
LockedAccounts = @()
DisabledAccounts = @()
}
foreach ($Account in $PrivilegedAccounts) {
# Check account expiry
if ($Account.AccountExpirationDate -and $Account.AccountExpirationDate -lt (Get-Date).AddDays($ExpiryWarningDays)) {
$LifecycleResults.ExpiringAccounts += $Account
}
# Check for inactive accounts
if ($Account.LastLogonDate -and $Account.LastLogonDate -lt (Get-Date).AddDays(-$InactiveAccountDays)) {
$LifecycleResults.InactiveAccounts += $Account
}
# Check password status
if ($Account.PasswordExpired) {
$LifecycleResults.PasswordExpired += $Account
}
# Check locked accounts
if ($Account.LockedOut) {
$LifecycleResults.LockedAccounts += $Account
}
# Check disabled accounts
if (-not $Account.Enabled) {
$LifecycleResults.DisabledAccounts += $Account
}
}
# Generate lifecycle report
$TotalIssues = ($LifecycleResults.Values | Measure-Object -Sum Count).Sum
$Html = @"
<!DOCTYPE html>
<html>
<head>
<title>Privileged Account Lifecycle Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.warning { background-color: #fff3cd; }
.critical { background-color: #f8d7da; }
.info { background-color: #d1ecf1; }
.header { background-color: #dc3545; color: white; padding: 10px; }
.summary { background-color: #f8f9fa; padding: 15px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>Privileged Account Lifecycle Report</h1>
<p>Generated: $(Get-Date)</p>
<p>Total Privileged Accounts: $($PrivilegedAccounts.Count)</p>
<p>Accounts Requiring Attention: $TotalIssues</p>
</div>
<div class="summary">
<h2>Lifecycle Summary</h2>
<p><strong>Expiring Accounts:</strong> $($LifecycleResults.ExpiringAccounts.Count)</p>
<p><strong>Inactive Accounts:</strong> $($LifecycleResults.InactiveAccounts.Count)</p>
<p><strong>Password Expired:</strong> $($LifecycleResults.PasswordExpired.Count)</p>
<p><strong>Locked Accounts:</strong> $($LifecycleResults.LockedAccounts.Count)</p>
<p><strong>Disabled Accounts:</strong> $($LifecycleResults.DisabledAccounts.Count)</p>
</div>
"@
# Add detailed sections for each category
foreach ($Category in $LifecycleResults.Keys) {
$Accounts = $LifecycleResults[$Category]
if ($Accounts.Count -gt 0) {
$SeverityClass = switch ($Category) {
'ExpiringAccounts' { 'warning' }
'InactiveAccounts' { 'warning' }
'PasswordExpired' { 'critical' }
'LockedAccounts' { 'critical' }
'DisabledAccounts' { 'info' }
}
$Html += @"
<h2>$($Category -replace '([A-Z])', ' $1').Trim()</h2>
<table>
<tr>
<th>Username</th>
<th>Display Name</th>
<th>Last Logon</th>
<th>Account Expiry</th>
<th>Password Last Set</th>
<th>Enabled</th>
</tr>
"@
foreach ($Account in $Accounts) {
$Html += @"
<tr class="$SeverityClass">
<td>$($Account.SamAccountName)</td>
<td>$($Account.DisplayName)</td>
<td>$($Account.LastLogonDate)</td>
<td>$($Account.AccountExpirationDate)</td>
<td>$($Account.PasswordLastSet)</td>
<td>$($Account.Enabled)</td>
</tr>
"@
}
$Html += "</table>"
}
}
$Html += @"
</body>
</html>
"@
$Html | Out-File -FilePath $ReportPath -Encoding UTF8
Write-Host "Privileged account lifecycle report generated: $ReportPath" -ForegroundColor Green
# Send alert if critical issues found
if (($LifecycleResults.PasswordExpired.Count + $LifecycleResults.LockedAccounts.Count) -gt 0 -and $AlertEmail -and $SMTPServer) {
$Subject = "CRITICAL: Privileged Account Issues Detected"
$Body = "Critical privileged account issues detected:`n`n"
$Body += "Password Expired: $($LifecycleResults.PasswordExpired.Count)`n"
$Body += "Locked Accounts: $($LifecycleResults.LockedAccounts.Count)`n`n"
$Body += "Please review the detailed report: $ReportPath"
Send-MailMessage -To $AlertEmail -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Priority High
}
return $LifecycleResults
}
catch {
Write-Error "Failed to process privileged account lifecycle: $($_.Exception.Message)"
}
}
Just-In-Time (JIT) Access Management
Temporal Access Control
# Implement Just-In-Time access for privileged groups
function Grant-JITAccess {
param(
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[string]$PrivilegedGroup,
[int]$AccessDurationHours = 8,
[string]$Justification,
[string]$ApproverEmail,
[string]$SMTPServer,
[switch]$RequireApproval = $true
)
try {
$RequestId = New-Guid
$AccessStart = Get-Date
$AccessEnd = $AccessStart.AddHours($AccessDurationHours)
# Log access request
Write-EventLog -LogName 'Application' -Source 'PAM-JIT' -EventId 2001 -EntryType Information -Message "JIT access requested: User=$Username, Group=$PrivilegedGroup, Duration=$AccessDurationHours hours, RequestId=$RequestId"
if ($RequireApproval -and $ApproverEmail -and $SMTPServer) {
# Send approval request
$Subject = "JIT Access Request - $Username to $PrivilegedGroup"
$Body = @"
JIT Access Request Details:
User: $Username
Privileged Group: $PrivilegedGroup
Duration: $AccessDurationHours hours
Justification: $Justification
Request ID: $RequestId
Requested Time: $AccessStart
To approve this request, reply with APPROVE-$RequestId
To deny this request, reply with DENY-$RequestId
"@
Send-MailMessage -To $ApproverEmail -Subject $Subject -Body $Body -SmtpServer $SMTPServer
Write-Host "JIT access request sent for approval. Request ID: $RequestId" -ForegroundColor Yellow
return @{
RequestId = $RequestId
Status = 'PendingApproval'
Username = $Username
Group = $PrivilegedGroup
RequestTime = $AccessStart
}
}
else {
# Grant immediate access (auto-approval scenario)
return Approve-JITAccess -RequestId $RequestId -Username $Username -PrivilegedGroup $PrivilegedGroup -AccessDurationHours $AccessDurationHours
}
}
catch {
Write-Error "Failed to process JIT access request: $($_.Exception.Message)"
}
}
# Approve JIT access request
function Approve-JITAccess {
param(
[Parameter(Mandatory)]
[string]$RequestId,
[Parameter(Mandatory)]
[string]$Username,
[Parameter(Mandatory)]
[string]$PrivilegedGroup,
[int]$AccessDurationHours = 8,
[string]$ApprovedBy = $env:USERNAME
)
try {
$AccessStart = Get-Date
$AccessEnd = $AccessStart.AddHours($AccessDurationHours)
# Add user to privileged group
Add-ADGroupMember -Identity $PrivilegedGroup -Members $Username
# Schedule automatic removal
$RemovalJob = Register-ScheduledJob -Name "JIT-Removal-$RequestId" -ScriptBlock {
param($Username, $PrivilegedGroup, $RequestId)
try {
Remove-ADGroupMember -Identity $PrivilegedGroup -Members $Username -Confirm:$false
Write-EventLog -LogName 'Application' -Source 'PAM-JIT' -EventId 2003 -EntryType Information -Message "JIT access automatically revoked: User=$Username, Group=$PrivilegedGroup, RequestId=$RequestId"
# Clean up scheduled job
Unregister-ScheduledJob -Name "JIT-Removal-$RequestId" -Force
}
catch {
Write-EventLog -LogName 'Application' -Source 'PAM-JIT' -EventId 2004 -EntryType Error -Message "Failed to revoke JIT access: User=$Username, Group=$PrivilegedGroup, RequestId=$RequestId, Error=$($_.Exception.Message)"
}
} -ArgumentList $Username, $PrivilegedGroup, $RequestId -RunNow:$false
# Set trigger for automatic removal
$Trigger = New-JobTrigger -Once -At $AccessEnd
Add-JobTrigger -InputObject $RemovalJob -Trigger $Trigger
# Log access grant
Write-EventLog -LogName 'Application' -Source 'PAM-JIT' -EventId 2002 -EntryType Information -Message "JIT access granted: User=$Username, Group=$PrivilegedGroup, Duration=$AccessDurationHours hours, ApprovedBy=$ApprovedBy, RequestId=$RequestId"
Write-Host "JIT access granted to $Username for $PrivilegedGroup until $AccessEnd" -ForegroundColor Green
return @{
RequestId = $RequestId
Status = 'Approved'
Username = $Username
Group = $PrivilegedGroup
AccessStart = $AccessStart
AccessEnd = $AccessEnd
ApprovedBy = $ApprovedBy
}
}
catch {
Write-Error "Failed to approve JIT access: $($_.Exception.Message)"
}
}
# Monitor and report on JIT access usage
function Get-JITAccessReport {
param(
[datetime]$StartDate = (Get-Date).AddDays(-30),
[datetime]$EndDate = (Get-Date),
[string]$ReportPath = "C:\Reports\JIT_Access_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
)
try {
# Query JIT access events from event log
$JITEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'PAM-JIT'
StartTime = $StartDate
EndTime = $EndDate
} -ErrorAction SilentlyContinue
$AccessSummary = @{
TotalRequests = 0
ApprovedRequests = 0
RevokedAccess = 0
ActiveSessions = 0
}
$AccessDetails = @()
foreach ($Event in $JITEvents) {
switch ($Event.Id) {
2001 { # Request
$AccessSummary.TotalRequests++
$AccessDetails += [PSCustomObject]@{
Type = 'Request'
Timestamp = $Event.TimeCreated
Message = $Event.Message
User = ($Event.Message -split 'User=')[1].Split(',')[0]
Group = ($Event.Message -split 'Group=')[1].Split(',')[0]
RequestId = ($Event.Message -split 'RequestId=')[1]
}
}
2002 { # Approval
$AccessSummary.ApprovedRequests++
$AccessDetails += [PSCustomObject]@{
Type = 'Approval'
Timestamp = $Event.TimeCreated
Message = $Event.Message
User = ($Event.Message -split 'User=')[1].Split(',')[0]
Group = ($Event.Message -split 'Group=')[1].Split(',')[0]
RequestId = ($Event.Message -split 'RequestId=')[1]
}
}
2003 { # Revocation
$AccessSummary.RevokedAccess++
$AccessDetails += [PSCustomObject]@{
Type = 'Revocation'
Timestamp = $Event.TimeCreated
Message = $Event.Message
User = ($Event.Message -split 'User=')[1].Split(',')[0]
Group = ($Event.Message -split 'Group=')[1].Split(',')[0]
RequestId = ($Event.Message -split 'RequestId=')[1]
}
}
}
}
# Generate JIT access report
$Html = @"
<!DOCTYPE html>
<html>
<head>
<title>Just-In-Time Access Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.request { background-color: #d1ecf1; }
.approval { background-color: #d4edda; }
.revocation { background-color: #fff3cd; }
.header { background-color: #28a745; color: white; padding: 10px; }
.summary { background-color: #f8f9fa; padding: 15px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>Just-In-Time Access Report</h1>
<p>Report Period: $StartDate to $EndDate</p>
</div>
<div class="summary">
<h2>JIT Access Summary</h2>
<p><strong>Total Requests:</strong> $($AccessSummary.TotalRequests)</p>
<p><strong>Approved Requests:</strong> $($AccessSummary.ApprovedRequests)</p>
<p><strong>Revoked Access:</strong> $($AccessSummary.RevokedAccess)</p>
<p><strong>Approval Rate:</strong> $(if ($AccessSummary.TotalRequests -gt 0) { [math]::Round(($AccessSummary.ApprovedRequests / $AccessSummary.TotalRequests) * 100, 2) } else { 0 })%</p>
</div>
<h2>JIT Access Activity</h2>
<table>
<tr>
<th>Type</th>
<th>Timestamp</th>
<th>User</th>
<th>Group</th>
<th>Request ID</th>
</tr>
"@
foreach ($Detail in $AccessDetails | Sort-Object Timestamp -Descending) {
$TypeClass = $Detail.Type.ToLower()
$Html += @"
<tr class="$TypeClass">
<td>$($Detail.Type)</td>
<td>$($Detail.Timestamp)</td>
<td>$($Detail.User)</td>
<td>$($Detail.Group)</td>
<td>$($Detail.RequestId)</td>
</tr>
"@
}
$Html += @"
</table>
</body>
</html>
"@
$Html | Out-File -FilePath $ReportPath -Encoding UTF8
Write-Host "JIT access report generated: $ReportPath" -ForegroundColor Green
return $AccessSummary
}
catch {
Write-Error "Failed to generate JIT access report: $($_.Exception.Message)"
}
}
Privileged Group Management
Advanced Group Policy Management
# Implement comprehensive privileged group management
function Set-PrivilegedGroupManagement {
param(
[string[]]$PrivilegedGroups = @(
'Domain Admins',
'Enterprise Admins',
'Schema Admins',
'Administrators',
'Server Operators',
'Backup Operators',
'Account Operators',
'Incoming Forest Trust Builders'
),
[string]$GPOName = 'PAM-RestrictedGroups-Policy',
[string]$TargetOU = 'OU=Domain Controllers,DC=domain,DC=com'
)
try {
# Create or update restricted groups GPO
$GPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue
if (-not $GPO) {
$GPO = New-GPO -Name $GPOName -Comment "Privileged Access Management - Restricted Groups Policy"
Write-Host "Created new GPO: $GPOName" -ForegroundColor Green
}
# Configure restricted groups
foreach ($Group in $PrivilegedGroups) {
try {
$GroupSID = (Get-ADGroup -Identity $Group).SID.Value
# Set restricted group policy (empty membership by default)
$GPOPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($GPO.Id)}\Machine\Microsoft\Windows NT\SecEdit"
if (-not (Test-Path $GPOPath)) {
New-Item -Path $GPOPath -ItemType Directory -Force
}
$GptTmplPath = Join-Path $GPOPath "GptTmpl.inf"
# Build restricted groups section
$RestrictedGroupsContent = @"
[Unicode]
Unicode=yes
[Group Membership]
*$GroupSID__Memberof =
*$GroupSID__Members =
[Version]
signature="`$CHICAGO`$"
Revision=1
"@
$RestrictedGroupsContent | Out-File -FilePath $GptTmplPath -Encoding Unicode
Write-Host "Configured restricted group policy for: $Group" -ForegroundColor Green
}
catch {
Write-Warning "Failed to configure restricted group policy for $Group`: $($_.Exception.Message)"
}
}
# Link GPO to target OU
try {
New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction SilentlyContinue
Write-Host "Linked GPO $GPOName to $TargetOU" -ForegroundColor Green
}
catch {
Write-Warning "Failed to link GPO: $($_.Exception.Message)"
}
Write-Host "Privileged group management policy configured successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to configure privileged group management: $($_.Exception.Message)"
}
}
# Monitor privileged group membership changes
function Monitor-PrivilegedGroupChanges {
param(
[string[]]$MonitoredGroups = @(
'Domain Admins',
'Enterprise Admins',
'Schema Admins',
'Administrators'
),
[string]$AlertEmail = $null,
[string]$SMTPServer = $null,
[int]$MonitoringInterval = 60 # seconds
)
# Store baseline membership
$BaselineMembership = @{}
foreach ($Group in $MonitoredGroups) {
try {
$Members = Get-ADGroupMember -Identity $Group -Recursive | Select-Object -ExpandProperty SamAccountName
$BaselineMembership[$Group] = $Members
Write-Host "Baseline membership for $Group`: $($Members.Count) members" -ForegroundColor Blue
}
catch {
Write-Warning "Failed to get baseline membership for $Group`: $($_.Exception.Message)"
}
}
# Start monitoring loop
while ($true) {
foreach ($Group in $MonitoredGroups) {
try {
$CurrentMembers = Get-ADGroupMember -Identity $Group -Recursive | Select-Object -ExpandProperty SamAccountName
$BaselineMembers = $BaselineMembership[$Group]
# Check for changes
$AddedMembers = $CurrentMembers | Where-Object { $_ -notin $BaselineMembers }
$RemovedMembers = $BaselineMembers | Where-Object { $_ -notin $CurrentMembers }
if ($AddedMembers -or $RemovedMembers) {
$ChangeMessage = "Privileged group membership change detected for $Group`:`n"
if ($AddedMembers) {
$ChangeMessage += "Added members: $($AddedMembers -join ', ')`n"
}
if ($RemovedMembers) {
$ChangeMessage += "Removed members: $($RemovedMembers -join ', ')`n"
}
# Log the change
Write-EventLog -LogName 'Application' -Source 'PAM-Monitor' -EventId 3001 -EntryType Warning -Message $ChangeMessage
Write-Host $ChangeMessage -ForegroundColor Yellow
# Send email alert
if ($AlertEmail -and $SMTPServer) {
$Subject = "ALERT: Privileged Group Membership Change - $Group"
Send-MailMessage -To $AlertEmail -Subject $Subject -Body $ChangeMessage -SmtpServer $SMTPServer -Priority High
}
# Update baseline
$BaselineMembership[$Group] = $CurrentMembers
}
}
catch {
Write-Error "Failed to monitor group $Group`: $($_.Exception.Message)"
}
}
Start-Sleep -Seconds $MonitoringInterval
}
}
Emergency Access Management
Break-Glass Procedures
# Create and manage emergency access (break-glass) accounts
function New-EmergencyAccessAccount {
param(
[Parameter(Mandatory)]
[string]$AccountName,
[string]$Description = "Emergency break-glass administrative account",
[string[]]$PrivilegedGroups = @('Domain Admins'),
[int]$AccountValidityDays = 90,
[switch]$EnableSmartCardRequirement
)
try {
$DomainDN = (Get-ADDomain).DistinguishedName
$EmergencyOU = "OU=EmergencyAccess,OU=Privileged Access Management,$DomainDN"
# Ensure emergency OU exists
if (-not (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$EmergencyOU'" -ErrorAction SilentlyContinue)) {
Write-Error "Emergency Access OU not found. Please create PAM OU structure first."
return
}
# Generate secure password for break-glass account
$SecurePassword = New-SecurePassword -Length 32 -IncludeSpecialCharacters
# Create emergency account
$AccountParams = @{
Name = $AccountName
SamAccountName = $AccountName
UserPrincipalName = "$AccountName@$((Get-ADDomain).DNSRoot)"
Path = $EmergencyOU
Description = "$Description - Created: $(Get-Date) - Expires: $((Get-Date).AddDays($AccountValidityDays))"
AccountPassword = $SecurePassword
ChangePasswordAtLogon = $false
Enabled = $true
PasswordNeverExpires = $false
SmartcardLogonRequired = $EnableSmartCardRequirement
AccountExpirationDate = (Get-Date).AddDays($AccountValidityDays)
TrustedForDelegation = $false
}
New-ADUser @AccountParams
# Add to privileged groups
foreach ($Group in $PrivilegedGroups) {
try {
Add-ADGroupMember -Identity $Group -Members $AccountName
Write-Host "Added $AccountName to $Group" -ForegroundColor Green
}
catch {
Write-Warning "Failed to add $AccountName to $Group`: $($_.Exception.Message)"
}
}
# Set emergency access auditing
$User = Get-ADUser -Identity $AccountName
$UserDN = $User.DistinguishedName
# Configure comprehensive auditing for emergency account
$AuditACL = Get-ACL -Path "AD:$UserDN" -Audit
$AuditRule = New-Object System.DirectoryServices.ActiveDirectoryAuditRule(
[System.Security.Principal.SecurityIdentifier]::new('S-1-1-0'), # Everyone
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AuditFlags]::Success,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
)
$AuditACL.SetAuditRule($AuditRule)
Set-ACL -Path "AD:$UserDN" -AclObject $AuditACL
# Log emergency account creation
Write-EventLog -LogName 'Application' -Source 'PAM-Emergency' -EventId 4001 -EntryType Information -Message "Emergency access account created: $AccountName, Groups: $($PrivilegedGroups -join ', '), Expires: $((Get-Date).AddDays($AccountValidityDays))"
Write-Host "Emergency access account created successfully: $AccountName" -ForegroundColor Green
Write-Host "Account expires: $((Get-Date).AddDays($AccountValidityDays))" -ForegroundColor Yellow
# Store secure password information
$PasswordStorage = @{
AccountName = $AccountName
Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword))
Created = Get-Date
Expires = (Get-Date).AddDays($AccountValidityDays)
Groups = $PrivilegedGroups
}
return $PasswordStorage
}
catch {
Write-Error "Failed to create emergency access account: $($_.Exception.Message)"
}
}
# Monitor emergency account usage
function Monitor-EmergencyAccountUsage {
param(
[string]$EmergencyAccountPattern = "*emergency*",
[string]$AlertEmail = $null,
[string]$SMTPServer = $null,
[int]$MonitoringInterval = 300 # 5 minutes
)
$EmergencyAccounts = Get-ADUser -Filter "Name -like '$EmergencyAccountPattern'" -Properties LastLogonDate, AccountExpirationDate
if ($EmergencyAccounts.Count -eq 0) {
Write-Warning "No emergency accounts found matching pattern: $EmergencyAccountPattern"
return
}
Write-Host "Monitoring $($EmergencyAccounts.Count) emergency accounts..." -ForegroundColor Blue
# Store last logon times
$LastKnownLogons = @{}
foreach ($Account in $EmergencyAccounts) {
$LastKnownLogons[$Account.SamAccountName] = $Account.LastLogonDate
}
while ($true) {
foreach ($Account in $EmergencyAccounts) {
try {
$CurrentAccount = Get-ADUser -Identity $Account.SamAccountName -Properties LastLogonDate, AccountExpirationDate
$LastKnownLogon = $LastKnownLogons[$Account.SamAccountName]
# Check for new logon
if ($CurrentAccount.LastLogonDate -and ($CurrentAccount.LastLogonDate -ne $LastKnownLogon)) {
$AlertMessage = @"
EMERGENCY ACCOUNT USAGE DETECTED!
Account: $($CurrentAccount.SamAccountName)
Last Logon: $($CurrentAccount.LastLogonDate)
Previous Logon: $LastKnownLogon
Account Expires: $($CurrentAccount.AccountExpirationDate)
This is an automated alert for break-glass account usage.
Please verify this access is authorized and document the reason.
"@
# Log the usage
Write-EventLog -LogName 'Application' -Source 'PAM-Emergency' -EventId 4002 -EntryType Warning -Message "Emergency account logon detected: $($CurrentAccount.SamAccountName) at $($CurrentAccount.LastLogonDate)"
Write-Host $AlertMessage -ForegroundColor Red
# Send immediate alert
if ($AlertEmail -and $SMTPServer) {
$Subject = "CRITICAL: Emergency Account Usage - $($CurrentAccount.SamAccountName)"
Send-MailMessage -To $AlertEmail -Subject $Subject -Body $AlertMessage -SmtpServer $SMTPServer -Priority High
}
# Update last known logon
$LastKnownLogons[$Account.SamAccountName] = $CurrentAccount.LastLogonDate
}
# Check for expired accounts
if ($CurrentAccount.AccountExpirationDate -and $CurrentAccount.AccountExpirationDate -lt (Get-Date)) {
Write-Warning "Emergency account expired: $($CurrentAccount.SamAccountName) - Expired: $($CurrentAccount.AccountExpirationDate)"
}
}
catch {
Write-Error "Failed to monitor emergency account $($Account.SamAccountName): $($_.Exception.Message)"
}
}
Start-Sleep -Seconds $MonitoringInterval
}
}
Privileged Access Workstations (PAWs)
PAW Implementation and Management
# Configure Privileged Access Workstation policies
function Set-PAWConfiguration {
param(
[string]$PAWGroupName = "PAW-Users",
[string]$PAWComputerGroupName = "PAW-Computers",
[string]$GPOName = "PAM-PAW-Configuration",
[string]$PAWOU = "OU=Workstations,OU=Privileged Access Management"
)
try {
# Create PAW security groups
foreach ($GroupName in @($PAWGroupName, $PAWComputerGroupName)) {
if (-not (Get-ADGroup -Filter "Name -eq '$GroupName'" -ErrorAction SilentlyContinue)) {
$GroupOU = "OU=Groups,OU=Privileged Access Management,$((Get-ADDomain).DistinguishedName)"
New-ADGroup -Name $GroupName -GroupScope Global -GroupCategory Security -Path $GroupOU -Description "PAW $($GroupName.Split('-')[1]) security group"
Write-Host "Created PAW group: $GroupName" -ForegroundColor Green
}
}
# Create PAW configuration GPO
$GPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue
if (-not $GPO) {
$GPO = New-GPO -Name $GPOName -Comment "Privileged Access Workstation Configuration Policy"
Write-Host "Created PAW GPO: $GPOName" -ForegroundColor Green
}
# Configure PAW security settings via registry
$PAWRegistrySettings = @{
# Disable USB storage
'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR\Start' = 4
# Disable CD/DVD autorun
'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\NoDriveTypeAutoRun' = 255
# Disable Windows Store
'HKLM\SOFTWARE\Policies\Microsoft\WindowsStore\DisableStoreApps' = 1
# Enable Windows Defender
'HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\DisableAntiSpyware' = 0
# Configure AppLocker
'HKLM\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Appx\EnforcementMode' = 1
}
foreach ($RegPath in $PAWRegistrySettings.Keys) {
$Value = $PAWRegistrySettings[$RegPath]
$KeyPath = $RegPath.Substring(0, $RegPath.LastIndexOf('\'))
$ValueName = $RegPath.Substring($RegPath.LastIndexOf('\') + 1)
Set-GPRegistryValue -Name $GPOName -Key $KeyPath -ValueName $ValueName -Type DWord -Value $Value
}
# Link GPO to PAW OU
New-GPLink -Name $GPOName -Target "$PAWOU,$((Get-ADDomain).DistinguishedName)" -LinkEnabled Yes -ErrorAction SilentlyContinue
Write-Host "PAW configuration completed successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to configure PAW settings: $($_.Exception.Message)"
}
}
Compliance and Auditing
Comprehensive PAM Auditing
# Generate comprehensive PAM compliance report
function New-PAMComplianceReport {
param(
[datetime]$StartDate = (Get-Date).AddDays(-30),
[datetime]$EndDate = (Get-Date),
[string[]]$ComplianceFrameworks = @('SOX', 'NIST', 'ISO27001'),
[string]$ReportPath = "C:\Reports\PAM_Compliance_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
)
try {
$DomainDN = (Get-ADDomain).DistinguishedName
$PAMOU = "OU=Privileged Access Management,$DomainDN"
# Collect PAM compliance data
$ComplianceData = @{
PrivilegedAccounts = Get-ADUser -SearchBase $PAMOU -Filter * -Properties LastLogonDate, AccountExpirationDate, PasswordLastSet, MemberOf
PrivilegedGroups = Get-ADGroup -Filter "AdminCount -eq 1" -Properties Members, ManagedBy
SecurityEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4728, 4729, 4732, 4733, 4756, 4757 # Group membership changes
StartTime = $StartDate
EndTime = $EndDate
} -ErrorAction SilentlyContinue
PAMEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'PAM-*'
StartTime = $StartDate
EndTime = $EndDate
} -ErrorAction SilentlyContinue
}
# Analyze compliance metrics
$ComplianceMetrics = @{
TotalPrivilegedAccounts = $ComplianceData.PrivilegedAccounts.Count
AccountsWithRecentActivity = ($ComplianceData.PrivilegedAccounts | Where-Object { $_.LastLogonDate -gt (Get-Date).AddDays(-30) }).Count
AccountsNearExpiry = ($ComplianceData.PrivilegedAccounts | Where-Object { $_.AccountExpirationDate -and $_.AccountExpirationDate -lt (Get-Date).AddDays(30) }).Count
EmptyPrivilegedGroups = ($ComplianceData.PrivilegedGroups | Where-Object { -not $_.Members }).Count
GroupMembershipChanges = $ComplianceData.SecurityEvents.Count
PAMSystemEvents = $ComplianceData.PAMEvents.Count
}
# Generate compliance report
$Html = @"
<!DOCTYPE html>
<html>
<head>
<title>Privileged Access Management Compliance Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.compliant { background-color: #d4edda; }
.warning { background-color: #fff3cd; }
.non-compliant { background-color: #f8d7da; }
.header { background-color: #6f42c1; color: white; padding: 10px; }
.framework { background-color: #e3f2fd; padding: 10px; margin: 10px 0; }
</style>
</head>
<body>
<div class="header">
<h1>Privileged Access Management Compliance Report</h1>
<p>Report Period: $StartDate to $EndDate</p>
<p>Compliance Frameworks: $($ComplianceFrameworks -join ', ')</p>
</div>
<h2>Executive Summary</h2>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
<th>Status</th>
</tr>
<tr class="$( if ($ComplianceMetrics.TotalPrivilegedAccounts -lt 50) { 'compliant' } else { 'warning' })">
<td>Total Privileged Accounts</td>
<td>$($ComplianceMetrics.TotalPrivilegedAccounts)</td>
<td>$( if ($ComplianceMetrics.TotalPrivilegedAccounts -lt 50) { 'Compliant' } else { 'Review Required' })</td>
</tr>
<tr class="$( if ($ComplianceMetrics.EmptyPrivilegedGroups -gt ($ComplianceData.PrivilegedGroups.Count * 0.5)) { 'compliant' } else { 'warning' })">
<td>Empty Privileged Groups</td>
<td>$($ComplianceMetrics.EmptyPrivilegedGroups) of $($ComplianceData.PrivilegedGroups.Count)</td>
<td>$( if ($ComplianceMetrics.EmptyPrivilegedGroups -gt ($ComplianceData.PrivilegedGroups.Count * 0.5)) { 'Compliant' } else { 'Review Required' })</td>
</tr>
<tr class="$( if ($ComplianceMetrics.AccountsNearExpiry -eq 0) { 'compliant' } else { 'warning' })">
<td>Accounts Near Expiry</td>
<td>$($ComplianceMetrics.AccountsNearExpiry)</td>
<td>$( if ($ComplianceMetrics.AccountsNearExpiry -eq 0) { 'Compliant' } else { 'Action Required' })</td>
</tr>
<tr class="compliant">
<td>Group Membership Changes</td>
<td>$($ComplianceMetrics.GroupMembershipChanges)</td>
<td>Monitored</td>
</tr>
<tr class="compliant">
<td>PAM System Events</td>
<td>$($ComplianceMetrics.PAMSystemEvents)</td>
<td>Logged</td>
</tr>
</table>
"@
# Add framework-specific compliance sections
foreach ($Framework in $ComplianceFrameworks) {
$Html += @"
<div class="framework">
<h3>$Framework Compliance Requirements</h3>
"@
switch ($Framework) {
'SOX' {
$Html += @"
<ul>
<li>✓ Privileged access controls implemented</li>
<li>✓ User access reviews and certification</li>
<li>✓ Segregation of duties enforced</li>
<li>✓ Audit trail for privileged actions</li>
<li>✓ Regular access recertification process</li>
</ul>
"@
}
'NIST' {
$Html += @"
<ul>
<li>✓ Access Control (AC) - Privileged account management</li>
<li>✓ Audit and Accountability (AU) - Comprehensive logging</li>
<li>✓ Identification and Authentication (IA) - Multi-factor authentication</li>
<li>✓ System and Communications Protection (SC) - Secure access methods</li>
</ul>
"@
}
'ISO27001' {
$Html += @"
<ul>
<li>✓ A.9.2.3 Management of privileged access rights</li>
<li>✓ A.9.2.5 Review of user access rights</li>
<li>✓ A.9.2.6 Removal or adjustment of access rights</li>
<li>✓ A.12.4.1 Event logging</li>
<li>✓ A.12.4.3 Administrator and operator logs</li>
</ul>
"@
}
}
$Html += "</div>"
}
$Html += @"
</body>
</html>
"@
$Html | Out-File -FilePath $ReportPath -Encoding UTF8
Write-Host "PAM compliance report generated: $ReportPath" -ForegroundColor Green
return $ComplianceMetrics
}
catch {
Write-Error "Failed to generate PAM compliance report: $($_.Exception.Message)"
}
}
Best Practices and Recommendations
PAM Implementation Best Practices
Tiered Administration Model
- Implement strict tier separation to prevent credential theft escalation
- Use separate accounts for each administrative tier
- Enforce network isolation between tiers
Just-In-Time Access
- Minimize standing privileges through temporal access controls
- Implement approval workflows for privileged access requests
- Automate access revocation after time-based expiry
Zero Trust Principles
- Assume breach and verify every access request
- Implement continuous verification and monitoring
- Use conditional access and risk-based authentication
Privileged Access Workstations (PAWs)
- Dedicated hardened workstations for administrative tasks
- Separate from regular user computing environment
- Enhanced monitoring and security controls
Comprehensive Monitoring
- Real-time monitoring of privileged account activities
- Automated alerting for suspicious behaviors
- Regular audit and compliance reporting
Security Recommendations
Multi-Factor Authentication
- Mandatory MFA for all privileged accounts
- Smart card or certificate-based authentication preferred
- Risk-based authentication for sensitive operations
Regular Access Reviews
- Quarterly reviews of privileged group memberships
- Annual certification of privileged access rights
- Automated removal of unused accounts
Emergency Procedures
- Well-defined break-glass procedures
- Secure storage of emergency access credentials
- Comprehensive audit trail for emergency access usage
Cloud Integration and Hybrid Scenarios
Azure AD Privileged Identity Management (PIM)
For hybrid environments, integrate on-premises PAM with Azure AD PIM:
Azure AD Connect Configuration
- Sync privileged accounts to Azure AD
- Configure seamless SSO for hybrid scenarios
- Implement password hash synchronization or pass-through authentication
PIM Integration
- Configure eligible assignments for cloud resources
- Implement Just-In-Time access for Azure resources
- Use Conditional Access for additional security controls
Hybrid Monitoring
- Centralized monitoring across on-premises and cloud
- Unified reporting and compliance dashboards
- Cross-platform security event correlation
Additional Resources
Microsoft Documentation
- Privileged Access Management for Active Directory Domain Services
- Securing Privileged Access
- Azure AD Privileged Identity Management
Security Frameworks
- NIST Special Publication 800-53
- CIS Controls for Privileged Account Management
- SANS Privileged Account Management
Tools and Solutions
- Microsoft Identity Manager (MIM)
- CyberArk Privileged Access Security
- BeyondTrust Privileged Access Management
This guide provides comprehensive PAM strategies for Active Directory environments. Regular review and updates ensure continued effectiveness and security posture.