Active · v0.3.1 · Windows rollout completed
PowerShellWindows ServerWindows Event LogWTS APITask SchedulerDPAPIHTTPSJSONRDP

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

Local RDP Session Agent architecture
01 Native Windows sources LocalSessionManager records lifecycle transitions; WTS exposes current RDP session state and client address when available.
02 RDP Session Agent PowerShell runtime executed as SYSTEM through Task Scheduler with no inbound listener or custom HTTP service.
03 Normalization + reconciliation Maps Event IDs 21/23/24/25, normalizes source_ip, prepares WTS snapshots, and preserves incremental checkpoints.
04A state.json Stores the EventRecordID checkpoint and the state required for snapshots and safe resume.
04B Remote Session API Receives authenticated events and snapshots over outbound HTTPS and consolidates central session state.
04C credential.dat + spool + logs DPAPI-protected per-server secret, durable pending batches, operational logs, and local runtime rollback copies.
05 Task Scheduler Runs every minute and performs WTS reconciliation at the configured interval without a resident custom service.

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 IDEventMeaning
21LOGONnew RDP session
23LOGOFFsession ended
24DISCONNECTsession remains open but disconnected
25RECONNECTdisconnected 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_ip appeared 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.