Problem
Centralized RDP monitoring requires collecting evidence that exists locally on every Windows Server without turning the monitoring stack into another privileged network service exposed from each host.
Windows provides strong native sources, but they answer different questions:
Event Log -> what happened and in which order
WTS -> which sessions exist right now
The first Agent versions already combined those two sources. Later, the project needed to preserve connection-origin evidence as well, without making that field mandatory and without breaking older servers or the existing API contract.
Solution
RDP Session Agent is the Windows collector for Remote Session API.
It runs locally as SYSTEM through Task Scheduler and relies only on components available on Windows:
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational;- Windows Terminal Services API (
wtsapi32.dll); - Windows PowerShell;
- DPAPI;
- Task Scheduler;
- outbound HTTPS.
The Agent exposes no listener, requires no inbound WinRM/SMB/WMI access, and never stores the API’s global read key.
Compatibility
The current supported target is:
Windows Server 2012 -> Windows Server 2022
Windows PowerShell 3.0+
Supporting older PowerShell versions influenced several implementation choices, including using compatible .NET APIs and calling native WTS functions through a small C# helper compiled at runtime.
Local architecture
Collection stays close to the source while history, the state machine, persistence, and correlation remain centralized in the API.
Lifecycle collection
The Agent reads:
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
And normalizes the main events:
| Event ID | Event | Meaning |
|---|---|---|
| 21 | LOGON | new RDP session |
| 23 | LOGOFF | session ended |
| 24 | DISCONNECT | session remains open but disconnected |
| 25 | RECONNECT | disconnected session becomes active again |
EventRecordID is used as the incremental checkpoint. After initialization, the Agent only asks Windows for records newer than the last confirmed record ID.
Why WTS snapshots exist
Event Log preserves chronology, but the system cannot assume that every lifecycle record will always be observed perfectly.
At a configured interval—five minutes by default—the Agent enumerates current sessions through WTS.
Only relevant RDP sessions in these states are included:
ACTIVE
DISCONNECTED
The API uses the snapshot for reconciliation. A session that still appears open centrally but no longer exists on Windows can therefore be closed with RECONCILIATION semantics.
Connection-origin capture
The 0.3.x line added optional source_ip evidence when Windows provides a usable address.
There are two sources.
Event Log
For LOGON and RECONNECT, LocalSessionManager can expose an Address field.
The Agent accepts valid IPv4/IPv6 and normalizes values such as these to null:
LOCAL;- loopback;
- unspecified addresses;
- malformed or partial addresses.
A missing IP does not invalidate the lifecycle event.
WTSClientAddress
Snapshots query WTSClientAddress through WTSQuerySessionInformation.
If that query fails, only source_ip becomes null; the snapshot remains valid.
This is intentional: origin is additional evidence, not a prerequisite for recognizing the session itself.
Origin is not identity
The address exposed by Windows may describe the RDP client rather than the final peer observed in another network layer.
NAT, VPN, and Remote Desktop Gateway can all change that interpretation.
The Agent therefore does not decide which physical device originated the session. It publishes the evidence it has and leaves temporal correlation to the central system.
The current version also does not invent source_port, because the Windows sources used by the collector do not expose a reliable client port.
Spool before transmission
The Agent is designed around the assumption that network failure is a normal possibility.
Before transmitting a batch:
1. collect events
2. persist the envelope in local spool
3. send it to the API
4. receive acknowledgement
5. advance the checkpoint
6. remove the acknowledged spool item
If HTTP delivery fails, the file remains on disk and the checkpoint for that batch does not advance.
On the next run, pending spool is replayed before newer Event Log records are collected.
Idempotency and replay
The Agent does not need to solve the ambiguity of “the API may have accepted the request, but the response was lost” by itself.
It can safely retry.
Remote Session API implements idempotent ingestion and returns counters such as:
accepted=3 duplicates=0
or, on replay:
accepted=0 duplicates=3
The key property is that replay does not recreate the session.
Local state
The default runtime root is:
C:\ProgramData\RdpSessionAgent\
Important artifacts include:
config.json: API URL,server_id, and operational parameters;credential.dat: Agent secret protected by DPAPI;state.json: Event Log checkpoint and last snapshot timestamp;spool\: telemetry not yet acknowledged;logs\: local execution history;rollback\: runtime backups created during updates;src\: the installed runtime actually executed by Task Scheduler.
The Git checkout and installed runtime are intentionally separate.
Per-server credentials
Every Windows Server receives its own:
server_id
Agent secret
The secret is protected with:
Windows DPAPI
DataProtectionScope.LocalMachine
Plaintext is not stored in config.json.
The installed directory ACL is restricted to:
SYSTEM;- local Administrators.
credential.dat belongs to one machine and should never be copied to another host.
Scheduled execution
Installation creates a Scheduled Task named:
RDP Session Agent
It runs:
- every minute;
- as
SYSTEM; - with elevated local privileges;
- from the installed copy under
C:\ProgramData\RdpSessionAgent.
Short periodic executions avoid requiring a custom resident Windows Service.
The operational problem with updating an installed Agent
Earlier versions relied on reusing Install-Agent.ps1 to refresh runtime files, but that installer also receives server configuration and the plaintext Agent secret.
That is not appropriate for routine rollout: the API stores only the secret hash, and recovering or re-entering the plaintext credential should not be required just to replace code.
Version 0.3.1 introduced a separate update path.
Update-Agent.ps1
The updater replaces only:
src\
VERSION
And preserves:
config.json
credential.dat
state.json
spool\
logs\
Before replacement, it creates a runtime backup under:
C:\ProgramData\RdpSessionAgent\rollback\
During the swap, the Scheduled Task is temporarily disabled/stopped so runtime files are not replaced in the middle of execution.
After the swap, the task returns to its previous enabled state.
If replacement fails after it has begun, the script attempts to restore the previous src and VERSION automatically.
Updating without the secret
The normal workflow is now:
git switch main
git pull --ff-only
.\scripts\Update-Agent.ps1
AgentSecret is not involved.
Install-Agent.ps1 remains reserved for:
- first installation;
- credential rotation;
- repair that genuinely needs to rewrite configuration or credential material.
This separation reduces operational risk and makes batch rollout much more predictable.
Validated rollout
Version 0.3.1 was first validated on canary servers across different Windows Server generations and then expanded gradually.
The gate exercised:
LOGON -> ACTIVE
DISCONNECT -> DISCONNECTED
RECONNECT -> ACTIVE
LOGOFF -> CLOSED
It also verified:
- Scheduled Task state remained correct;
- existing authentication continued to work;
- WTS reconciliation remained healthy;
- spool did not grow;
source_ipappeared when Windows provided usable evidence;- reconnect did not duplicate sessions;
- runtime rollback remained possible without copying credentials.
After canary validation, the release was rolled out to all monitored Windows servers without observed operational regressions.
Preflight and operations
Before first installation, the project checks:
- PowerShell version;
- LocalSessionManager availability;
- WTS enumeration;
- API HTTPS health;
- certificate trust.
Routine inspection can use:
$Root = 'C:\ProgramData\RdpSessionAgent'
Get-Content "$Root\VERSION"
schtasks.exe /Query /TN 'RDP Session Agent' /V /FO LIST
Get-Content "$Root\logs\agent-$(Get-Date -Format yyyyMMdd).log" -Tail 50
Get-ChildItem "$Root\spool" -File
A healthy Agent should have an enabled task, fresh logs, periodic snapshots, stable spool, and advancing API last_seen.
Security and privacy
The Agent collects session metadata, not user content.
It does not capture:
- commands;
- clipboard data;
- passwords;
- session contents;
- transferred files.
It also requires no inbound central-management service.
The monitored host remains an outbound telemetry producer instead of turning the central platform into a privileged remote operator across every Windows Server.
Current state
Version 0.3.1 combines:
- RDP lifecycle collection through Event Log;
- WTS reconciliation;
- optional IPv4/IPv6 origin evidence;
- durable spool;
- idempotent replay;
- local checkpointing;
- machine-scoped DPAPI;
- Scheduled Task execution;
- secret-safe runtime updates;
- runtime rollback;
- Windows Server 2012–2022 compatibility target.
What the project demonstrates
RDP Session Agent started as a small PowerShell collector, but its evolution highlights an important infrastructure lesson: collecting telemetry is only half the problem; safely updating and recovering the collector is part of the system too.
The project demonstrates native Windows API integration, incremental Event Log processing, state reconciliation, failure-oriented local persistence, DPAPI-based secret handling, and backward-compatible rollout on real servers.
Ready to read this content aloud.