How to Implement Just-In-Time (JIT) Administration on Windows Server 2025
Traditional privileged accounts represent one of the most significant attack surfaces in any Windows environment. When an administrator account holds permanent membership in Domain Admins or Enterprise Admins, a single compromised credential can devastate an entire forest. Just-In-Time (JIT) administration solves this by granting elevated privileges only when needed, for a defined period, and then automatically revoking them. Windows Server 2025 supports JIT administration through multiple mechanisms: Azure Active Directory Privileged Identity Management (PIM) for hybrid and cloud-joined scenarios, Microsoft Identity Manager Privileged Access Management (MIM PAM) for fully on-premises environments, and PowerShell-based tooling for lightweight implementations. This tutorial walks through each approach so you can select the model that fits your infrastructure.
Prerequisites
- Windows Server 2025 domain controller at forest functional level 2016 or higher
- For Azure AD PIM: Azure AD P2 or Microsoft Entra ID Governance licenses for all privileged users
- For MIM PAM: Microsoft Identity Manager 2016 SP2 with PAM hotfix rollup, a dedicated bastion forest
- PowerShell 5.1 or PowerShell 7.x and the ActiveDirectory module
- An account with Domain Admin rights to configure shadow groups and GPOs
- Multi-factor authentication (MFA) already enforced for admin accounts
Step 1: Understanding the JIT Model and Protected Users
Before configuring JIT, add all privileged accounts to the Protected Users security group. This built-in group, available since Windows Server 2012 R2, enforces a hardened Kerberos policy: no NTLM authentication, no DES or RC4 Kerberos keys, tickets limited to 4-hour lifetimes, and credentials never cached. Protected Users is the essential baseline that makes JIT meaningful — even if a ticket is stolen, it expires quickly.
# Add privileged accounts to Protected Users
$privilegedUsers = @("AdminAlice", "AdminBob", "SvcAcctDeploy")
foreach ($user in $privilegedUsers) {
Add-ADGroupMember -Identity "Protected Users" -Members $user
Write-Host "Added $user to Protected Users" -ForegroundColor Green
}
# Verify membership
Get-ADGroupMember -Identity "Protected Users" | Select-Object Name, SamAccountName
After this step, these accounts can no longer be used for pass-the-hash or pass-the-ticket attacks. They must authenticate interactively with full Kerberos.
Step 2: Implementing JIT with Azure AD Privileged Identity Management (PIM)
Azure AD PIM is the recommended approach for organizations with hybrid or cloud-native identity. Users are assigned eligible roles rather than permanent ones. When they need elevated access, they activate the role in the Entra portal or via PowerShell, provide an MFA proof and a business justification, and receive time-limited role membership — typically 1–8 hours — that auto-expires.
# Install the Microsoft Graph PowerShell module
Install-Module Microsoft.Graph -Scope CurrentUser -Force
# Connect with PIM management scopes
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory","RoleEligibilitySchedule.ReadWrite.Directory"
# List all built-in directory roles to find the role definition ID
Get-MgRoleManagementDirectoryRoleDefinition | Where-Object { $_.DisplayName -like "*Global Admin*" } |
Select-Object DisplayName, Id
# Create an eligible role assignment (does NOT grant the role yet — requires activation)
$params = @{
action = "adminAssign"
justification = "JIT eligible assignment for server provisioning team"
roleDefinitionId = ""
directoryScopeId = "/" # tenant-wide; use /administrativeUnits/ for scoped
principalId = (Get-MgUser -Filter "userPrincipalName eq '[email protected]'").Id
scheduleInfo = @{
startDateTime = (Get-Date).ToUniversalTime().ToString("o")
expiration = @{
type = "noExpiration" # eligible assignment itself doesn't expire
endDateTime = $null
}
}
}
New-MgRoleManagementDirectoryRoleEligibilityScheduleRequest -BodyParameter $params
Once eligible, the user activates their role from the My roles blade in https://entra.microsoft.com, or via PowerShell:
# User self-activates an eligible role (run as the eligible user)
Connect-MgGraph -Scopes "RoleAssignmentSchedule.ReadWrite.Directory"
$activationParams = @{
action = "selfActivate"
principalId = (Get-MgUser -Filter "userPrincipalName eq '[email protected]'").Id
roleDefinitionId = ""
directoryScopeId = "/"
justification = "Patching production DC cluster — CHG0012345"
scheduleInfo = @{
startDateTime = (Get-Date).ToUniversalTime().ToString("o")
expiration = @{
type = "afterDuration"
duration = "PT2H" # 2-hour window
}
}
}
New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $activationParams
Write-Host "Role activated for 2 hours. It will auto-expire." -ForegroundColor Yellow
Step 3: On-Premises JIT with Microsoft Identity Manager PAM
For air-gapped or fully on-premises environments, MIM PAM provides equivalent functionality using a bastion forest model. A dedicated, hardened Active Directory forest (the bastion) holds shadow copies of privileged groups. When a user needs elevated access to a production forest resource, MIM grants them time-limited membership in the shadow group — which is linked to the production group via a cross-forest trust — and removes it automatically after the configured TTL.
# On the MIM PAM server: request privileged access for a user
# This uses the MIM PAM PowerShell cmdlets (MIMWAL + PAM component)
Import-Module MIMPAM
# View available PAM roles
Get-PAMRole | Select-Object DisplayName, TTL, AvailableFrom, AvailableTo
# Request access to the "ServerAdmins-JIT" PAM role for 4 hours
$pamRole = Get-PAMRole -DisplayName "ServerAdmins-JIT"
New-PAMRequest -Role $pamRole -Justification "Deploying hotfix KB5040442 — INC0087654" -TTL 04:00:00
# Check current active PAM requests
Get-PAMRequest -Filter "RequestorAccountName -eq 'BASTIONadminAlice'" |
Select-Object CreatedTime, ExpirationTime, Status, RoleDisplayName
MIM PAM automatically creates and removes shadow security group memberships in the bastion forest. The production forest trusts the bastion forest (one-way trust), so membership in bastion groups translates to effective access in production without permanent group membership there.
Step 4: Lightweight PowerShell-Based JIT Without MIM
For smaller environments without MIM PAM, you can implement a basic JIT model using a scheduled task or a simple daemon that expires group memberships based on a timestamp stored in the user’s AD attribute (e.g., extensionAttribute1).
# JIT-Grant.ps1 — grant timed group membership and record expiry
param(
[Parameter(Mandatory)] [string]$UserSamAccountName,
[Parameter(Mandatory)] [string]$GroupName,
[int]$DurationMinutes = 60,
[string]$Justification = "No justification provided"
)
$expiry = (Get-Date).AddMinutes($DurationMinutes).ToString("o")
$user = Get-ADUser -Identity $UserSamAccountName
# Store expiry in extensionAttribute1 (format: GroupName|ISO8601expiry)
Set-ADUser -Identity $UserSamAccountName -Add @{ extensionAttribute1 = "$GroupName|$expiry" }
# Add to the privileged group
Add-ADGroupMember -Identity $GroupName -Members $UserSamAccountName
# Log the grant
$logEntry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] GRANT | User=$UserSamAccountName | Group=$GroupName | Expiry=$expiry | Reason=$Justification"
Add-Content -Path "C:LogsJIT-Admin.log" -Value $logEntry
Write-Host "Access granted to $GroupName until $expiry" -ForegroundColor Green
Write-Warning "Ensure JIT-Expire.ps1 is running as a scheduled task every 5 minutes."
# JIT-Expire.ps1 — scheduled task, runs every 5 minutes, removes expired memberships
Import-Module ActiveDirectory
$now = Get-Date
# Find all users with extensionAttribute1 set (active JIT grants)
$jitUsers = Get-ADUser -Filter { extensionAttribute1 -like "*|*" } -Properties extensionAttribute1
foreach ($user in $jitUsers) {
$entries = $user.extensionAttribute1 -split ";"
foreach ($entry in $entries) {
$parts = $entry -split "|"
if ($parts.Count -lt 2) { continue }
$group = $parts[0]
$expiry = [datetime]::Parse($parts[1])
if ($now -gt $expiry) {
try {
Remove-ADGroupMember -Identity $group -Members $user.SamAccountName -Confirm:$false
$logEntry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] EXPIRE | User=$($user.SamAccountName) | Group=$group"
Add-Content -Path "C:LogsJIT-Admin.log" -Value $logEntry
Write-Host "Expired: $($user.SamAccountName) removed from $group"
} catch {
Write-Warning "Failed to remove $($user.SamAccountName) from $group : $_"
}
}
}
}
Register the expiry script as a scheduled task running every 5 minutes under a service account with sufficient rights to modify group membership:
# Register the JIT expiry scheduled task
$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-NonInteractive -File C:ScriptsJIT-Expire.ps1"
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 5) -Once -At (Get-Date)
$principal = New-ScheduledTaskPrincipal -UserId "CONTOSOsvc-jit-expire" -LogonType Password -RunLevel Highest
Register-ScheduledTask -TaskName "JIT-AdminExpiry" -Action $action -Trigger $trigger -Principal $principal -Description "Auto-expire JIT admin group memberships"
Step 5: Comparing MIM PAM vs Azure AD PIM
Choose the right tool based on your environment:
- Azure AD PIM: Best for hybrid or cloud-native environments with Entra ID P2 licenses. Provides full audit trail in the Entra portal, approval workflows, alerting, and access reviews. No on-premises infrastructure required for cloud roles. For on-premises AD groups, use PIM for Groups (Entra ID Governance).
- MIM PAM: Best for air-gapped, highly regulated, or fully on-premises environments. Requires a dedicated bastion forest — significant infrastructure overhead — but provides equivalent capability entirely within your data center.
- PowerShell JIT: Suitable for small environments or as an interim solution. Requires careful implementation to avoid race conditions and ensure the expiry daemon is always running. Not recommended for large-scale deployments.
Step 6: Auditing and Alerting JIT Activity
# Query Security event log for privileged group changes (Event ID 4728 = member added to global group)
Get-WinEvent -FilterHashtable @{
LogName = "Security"
Id = 4728, 4729 # 4729 = member removed
StartTime = (Get-Date).AddDays(-1)
} | ForEach-Object {
$xml = [xml]$_.ToXml()
$data = $xml.Event.EventData.Data
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
EventId = $_.Id
SubjectUser = ($data | Where-Object { $_.Name -eq "SubjectUserName" }).'#text'
MemberName = ($data | Where-Object { $_.Name -eq "MemberName" }).'#text'
GroupName = ($data | Where-Object { $_.Name -eq "TargetUserName" }).'#text'
}
} | Format-Table -AutoSize
Implementing Just-In-Time administration on Windows Server 2025 significantly reduces your privileged attack surface by ensuring that elevated permissions exist only when actively needed and are automatically revoked once the approved window closes. Whether you deploy Azure AD PIM for its rich portal and approval workflows, MIM PAM for fully on-premises control, or a lightweight PowerShell approach, pairing JIT with the Protected Users group and comprehensive audit logging gives you a defence-in-depth posture that makes credential theft far less catastrophic. Review your JIT logs weekly and run Entra access reviews quarterly to ensure only the right people retain eligibility.