The Night a DNS Admin Became a Domain Admin
It was 11 PM on a Tuesday when the SIEM flagged a credential harvesting alert on a domain controller. Someone had launched Mimikatz from a PowerShell remoting session. The session belonged to a DNS administrator, a member of a team that had no business running arbitrary commands on a DC. But because the default PowerShell remoting endpoint grants full access to anyone in the local Administrators group, that DNS admin account was a skeleton key to the entire domain. The attacker had compromised one set of credentials and turned them into an unrestricted remote shell.
That engagement taught us something we already knew but had not enforced. PowerShell JEA exists precisely to prevent this scenario. Just Enough Administration locks down PowerShell remoting endpoints so that users can only run the exact commands their role requires. Nothing more. The rest of this article walks through how we rebuilt that client’s remote access model from scratch using JEA, including the mistakes we made along the way.
Why Default Remoting Endpoints Are a Detection Blind Spot
When you connect to a remote machine with Enter-PSSession or Invoke-Command, you land in the default endpoint. That endpoint is wide open. If you are an administrator on the target machine, you can run any command, access any provider, and interact with any module installed on the system. From a MITRE ATT&CK perspective, this is Tactic T1021.006, Remote Services: Windows Remote Management. An attacker who compromises any account with local admin rights gets a full interactive shell.
The detection problem is that legitimate admin sessions and malicious sessions look identical in the logs. Both are authenticated PowerShell remoting connections. Both execute commands. Without JEA, your SOC has no way to distinguish a DNS admin running Get-DnsServerZone from the same account running Invoke-Mimikatz. Alert fatigue multiplies because every remoting session becomes a potential indicator of compromise.
JEA changes this equation by creating custom endpoints where the allowed command set is explicitly defined. Any command outside the allowed list simply does not exist from the user’s perspective. They cannot see it with Get-Command, they cannot tab-complete it, and they cannot execute it. That is a security boundary you can actually monitor.
Setting the Scene: What We Were Building and Why
The client was a mid-size financial services firm with 12 domain controllers across three sites. Their DNS team of four people had Domain Admins membership because years earlier, someone decided that was the easiest way to let them manage DNS zones remotely. Nobody revisited the decision. We were brought in after the incident to implement least privilege access across their Active Directory infrastructure.
The goal was straightforward. Build a JEA endpoint on every domain controller that allows DNS administrators to manage DNS services and nothing else. Remove their Domain Admins membership. Close the default remoting endpoint to non-essential accounts. We gave ourselves a two-week window.
JEA requires two files to function. The role capability file, stored with a .psrc extension, defines what commands a user can run inside the constrained session. The session configuration file, stored with a .pssc extension, ties roles to user groups and controls the session behavior. Both files are PowerShell hash tables, and both have dedicated cmdlets for creation.
Building the Role Capability File
We started with the role capability file because it forces you to answer the most important question first: what exactly does this team need to do? Not what they are used to doing. What they actually need.
After a week of auditing the DNS team’s activity logs, we had a clear picture. They needed to view storage information, manage DNS server zones and records, and occasionally restart the DNS service. That was it. Everything else was habit or convenience, not necessity.
Creating the PSRC File
PowerShell provides New-PSRoleCapabilityFile to scaffold the file. We created a module directory structure first because JEA expects role capability files to live inside a module’s RoleCapabilities folder.
# Create the module directory structure for JEA role capabilities
$ModulePath = "$env:ProgramFiles\WindowsPowerShell\Modules\DnsAdminJEA"
New-Item -Path "$ModulePath\RoleCapabilities" -ItemType Directory -Force
# Generate the role capability file
New-PSRoleCapabilityFile -Path "$ModulePath\RoleCapabilities\DnsAdmin.psrc"
The generated file is a template full of commented-out options. We edited it to define the allowed command set. This is where most of the security decisions happen.
Defining Allowed Commands with Parameter Restrictions
The first instinct is to whitelist entire modules. Resist that instinct. I have seen JEA deployments fail security review because someone allowed all cmdlets from a module that included destructive commands. Specify individual cmdlets or use wildcard patterns that match only safe operations.
# Define the cmdlets the DNS admin role is allowed to use
$VisibleCmdlets = @(
# Allow all Get commands from the Storage module (read-only)
'Storage\Get-*'
# Allow Get-Item and Get-ItemProperty for file system inspection
'Microsoft.PowerShell.Management\Get-Item*'
# Allow DNS server management cmdlets
'DnsServer\Get-DnsServer*'
'DnsServer\Set-DnsServerZone'
'DnsServer\Add-DnsServerResourceRecord'
'DnsServer\Remove-DnsServerResourceRecord'
)
But raw cmdlet whitelisting is only half the story. Some cmdlets are safe in principle but dangerous with certain parameters. Stop-Process is a good example. You might want the DNS admin to kill a stuck DNS process, but you do not want them killing lsass.exe. JEA lets you lock down individual parameters with validation sets.
# Allow Stop-Process but only for specific process names
$VisibleCmdlets += @{
Name = 'Stop-Process'
Parameters = @{
Name = 'Name'
ValidateSet = 'dns', 'dnscache'
},
@{
Name = 'Force'
ValidateSet = $true, $false
}
}
# Allow Restart-Service but only for services matching the DNS pattern
$VisibleCmdlets += @{
Name = 'Restart-Service'
Parameters = @{
Name = 'Name'
ValidatePattern = 'DNS\w*'
}
}
The ValidateSet parameter locks the value to a predefined list. ValidatePattern uses regex to constrain input. Both are enforced by the JEA session itself, not by the cmdlet. Even if the cmdlet would normally accept any input, JEA blocks anything outside the validation rule.
This level of parameter control is what separates JEA from simple RBAC. You are not just controlling which tools someone can use. You are controlling how they can use them.
Building the Session Configuration File
With the role capability file complete, we moved to the session configuration file. This is the file that ties everything together: who connects, what role they get, and how the session behaves.
# Create the session configuration file
$SessionParams = @{
Path = '.\DnsAdminsJEA.pssc'
SessionType = 'RestrictedRemoteServer'
RunAsVirtualAccount = $true
TranscriptDirectory = 'C:\JEATranscripts\DnsAdmins'
RoleDefinitions = @{
'CONTOSO\DNS-Admins' = @{ RoleCapabilities = 'DnsAdmin' }
}
}
New-PSSessionConfigurationFile @SessionParams
Why RestrictedRemoteServer Matters
The SessionType parameter is critical. Setting it to RestrictedRemoteServer puts the session into NoLanguage mode. This severely restricts what the connected user can do beyond the explicitly allowed cmdlets. In NoLanguage mode, the user cannot define variables, run scripts, use pipelines beyond basic command execution, or access .NET classes directly. The session automatically provides a small set of essential cmdlets that make the session functional:
Clear-Host(cls, clear)Exit-PSSession(exsn, exit)Get-Command(gcm)Get-HelpGet-FormatDataMeasure-Object(measure)Out-DefaultSelect-Object(select)
You do not need to include these in your role capability file. They are provided automatically when you use RestrictedRemoteServer. I strongly recommend against using Default or Empty session types for JEA. If you are not using RestrictedRemoteServer, you are not really doing JEA.
Virtual Accounts: The Run-As Identity
The RunAsVirtualAccount parameter is what makes JEA operationally viable. When set to $true, the JEA session creates a temporary local virtual account with administrative privileges on the target machine. The connected user authenticates with their own credentials, but the commands they execute run under this virtual account.
This solves a real operational problem. The DNS admin needs elevated privileges to modify DNS zones on the domain controller. But you do not want to grant those privileges to their personal account. The virtual account holds the privileges. JEA holds the boundary around what the virtual account can do. The DNS admin’s actual account never gains local admin rights.
If you cannot use virtual accounts due to group Managed Service Account requirements or cross-domain scenarios, JEA also supports gMSA accounts. But for single-domain environments, virtual accounts are simpler to manage and do not require additional infrastructure.
Transcript Logging
Notice the TranscriptDirectory parameter. Every command executed in the JEA session is logged to a transcript file in that directory. From a SOC perspective, this is gold. You get a complete record of every command, its parameters, the output, the timestamp, and the connected user identity. Forward these transcripts to your SIEM and you have an audit trail that actually tells you what happened during a remote administration session.
We pipe these into our detection pipeline alongside the AD audit policies we configure for every client. The combination of JEA transcripts and Windows event logs gives you layered visibility into administrative activity on domain controllers.
The Mistake: Forgetting About Provider Access
Here is where the walkthrough gets honest. We built the role capability file, created the session configuration, and registered the endpoint. Everything tested clean. Then a DNS admin called in because they could not browse to a configuration file they needed to check on the DC.
By default, in a JEA session, you have no access to any PowerShell provider. That means no file system navigation, no registry access, no certificate store browsing. Nothing. The allowed cmdlets we defined could retrieve items, but the user could not use Set-Location or Get-ChildItem to browse directories because the FileSystem provider was not exposed.
We had to go back and add provider visibility to the role capability file:
# Add visible providers to the role capability file
# Only expose FileSystem — no Registry, no Certificate store
$RoleCapability = @{
VisibleProviders = 'FileSystem'
VisibleCmdlets = $VisibleCmdlets
# Restrict the file system paths they can access
VisibleFunctions = @{
Name = 'TabExpansion2'
}
}
Set-Content -Path "$ModulePath\RoleCapabilities\DnsAdmin.psrc" -Value ($RoleCapability | ConvertTo-Expression)
This was a lesson in testing beyond the happy path. We tested whether the allowed commands worked. We did not test whether the user could do the ancillary tasks that support those commands. Build your test cases around workflows, not individual cmdlets.
Registering and Testing the JEA Endpoint
With both files corrected, we registered the endpoint on each domain controller. This is the step that makes JEA active on the target machine.
Validating the Configuration
Before registering, always validate the session configuration file. A malformed file will fail silently or produce confusing errors during connection.
# Validate the session configuration file before registration
Test-PSSessionConfigurationFile -Path '.\DnsAdminsJEA.pssc'
# Returns True if the file is valid
Enabling Remoting and Registering the Endpoint
# Ensure PS Remoting is enabled on the target server
Enable-PSRemoting -Force | Out-Null
# Register the JEA session configuration as a new endpoint
$RegisterParams = @{
Path = '.\DnsAdminsJEA.pssc'
Name = 'DnsAdminsJEA'
Force = $true
}
Register-PSSessionConfiguration @RegisterParams
After registration, verify the endpoint exists alongside the default endpoints:
# List all remoting endpoints and their Run As accounts
Get-PSSessionConfiguration |
Format-Table -Property Name, PSVersion, RunAsAccount
You should see your DnsAdminsJEA endpoint listed with a virtual account reference in the RunAsAccount column.
Verifying User Capabilities Before Going Live
This step saved us from a second round of corrections. Get-PSSessionCapability lets you preview exactly what a specific user will be able to do in the JEA session without the user needing to connect.
# Check what JerryG from the DNS-Admins group can do in this endpoint
$CapabilityCheck = @{
ConfigurationName = 'DnsAdminsJEA'
Username = 'CONTOSO\JerryG'
}
Get-PSSessionCapability @CapabilityCheck | Sort-Object -Property Module
This outputs every command available to that user in the JEA session, sorted by module. If a command appears that should not be there, your role capability file needs tightening. If a required command is missing, you need to add it. Run this check for every user or group before announcing the endpoint to the team.
Connecting to the JEA Endpoint
With the endpoint registered and validated, users connect by specifying the configuration name in their remoting session:
# Connect to the JEA-restricted endpoint on DC01
Enter-PSSession -ComputerName DC01 `
-ConfigurationName 'DnsAdminsJEA' `
-Credential (Get-Credential)
Once inside, the user sees only the commands defined in their role capability file plus the small set of built-in session commands. If they try to run something outside the allowed set, they get an error stating the command is not recognized. They cannot even see that other commands exist on the system.
This is the principle of least privilege enforced at the protocol level, not the permission level. The user is not denied access to commands. The commands do not exist in their session. That is a meaningful distinction for both security and usability.
When testing is complete, exit cleanly:
Exit-PSSession
Scaling JEA Across Multiple Domain Controllers
We rolled this configuration out across all 12 domain controllers using PowerShell DSC. JEA supports both Windows PowerShell and PowerShell 7 running on Windows, so the same configuration works regardless of which version the target machine runs. The role capability file and session configuration file are just files. You deploy them to the correct module path and register the endpoint.
For organizations managing more than a handful of servers, I recommend packaging JEA configurations as PowerShell modules and distributing them through your existing configuration management tool, whether that is DSC, Ansible, or Group Policy. The role capability files go into the module’s RoleCapabilities folder. The session configuration file travels alongside and gets registered during deployment.
One caveat worth stating clearly. JEA only restricts the PowerShell remoting endpoint. If a user can RDP to the domain controller, they bypass JEA entirely. You must restrict all other forms of remote access, including Remote Desktop, to the target server for JEA to function as a real security boundary. We disabled RDP for non-tier-zero accounts on every DC as part of the same engagement. If you deploy JEA without closing RDP, you have built a locked front door with an open window. Related forensic controls like USB device tracking should be in place to cover physical access vectors as well.
Detection Engineering with JEA Transcripts
From a SOC perspective, JEA transcripts are a direct feed into your detection pipeline. Each transcript file includes the connected user, the virtual account identity, every command executed, and the timestamp. Here is a sample KQL query for surfacing JEA sessions where commands were blocked, which may indicate reconnaissance or credential compromise:
// Detect blocked command attempts in JEA sessions
// Indicates a user trying to run commands outside their allowed set
Event
| where EventLog == "Windows PowerShell"
| where EventID == 800
| where RenderedDescription has "is not recognized"
| where RenderedDescription has "JEA"
| project TimeGenerated, Computer, UserName, RenderedDescription
| sort by TimeGenerated desc
A blocked command in a JEA session is not necessarily malicious. Users hit the boundary sometimes through habit or misunderstanding. But a pattern of blocked commands, especially commands associated with credential access (T1003) or discovery (T1087), should trigger an escalation. JEA gives your SOC a clean signal to build detections around because the boundary between normal and abnormal is explicitly defined by you.
What We Would Do Differently Next Time
Three things stand out looking back at this deployment.
First, we should have built the role capability file collaboratively with the DNS team from the start. We spent a week auditing their logs to figure out what they needed. A two-hour working session with the team would have produced the same list in a fraction of the time and with higher accuracy. Operations teams know their workflows. Use that knowledge.
Second, we should have created separate role capability files for junior and senior DNS administrators. JEA supports multiple roles per endpoint. A junior admin might only need Get-* cmdlets for monitoring, while a senior admin needs Set-* and Add-* for changes. We flattened everyone into one role for speed, and the client later asked us to split them. Build for role granularity from the start.
Third, we underestimated transcript volume. Twelve domain controllers with four DNS admins generating transcripts for every session filled a disk in three weeks. Plan your transcript retention and rotation before you deploy, not after you get a disk space alert at 3 AM. Align it with your CIS Benchmark audit log retention requirements.
The Operational Takeaway
PowerShell JEA is not optional for any organization that allows remote administration of domain controllers, member servers, or any tier-zero asset. The default remoting endpoint is an open door. JEA replaces it with a controlled checkpoint where every allowed action is defined, logged, and auditable.
The deployment itself is not complex. Two files, a registration command, and a validation check. The hard part is the role design: determining exactly which commands each team needs and restricting everything else. Invest the time there. Get it right. The security payoff is immediate and measurable in your mean time to detect metrics.
If your organization is running PowerShell remoting without JEA, you have an unmonitored lateral movement path on every server that accepts connections. That is a finding on every assessment we run. If you need help scoping a JEA deployment or tightening your remote administration model, reach out to our team. We have deployed this across environments ranging from 10 to 400 servers, and the pattern scales well.


