Skip to content

Instantly share code, notes, and snippets.

@mayerwin
Last active July 28, 2025 07:48
Show Gist options
  • Select an option

  • Save mayerwin/153839aa5184cbf7a3fd3b07423c4f0c to your computer and use it in GitHub Desktop.

Select an option

Save mayerwin/153839aa5184cbf7a3fd3b07423c4f0c to your computer and use it in GitHub Desktop.
Headless RDP auto‑logon at startup (Windows Server 2022+). Creates a hidden FreeRDP connection at boot (to fire user logon triggers), then kills it after 60 s. Stores creds securely in Credential Manager.
<#
.SYNOPSIS
Installs (or uninstalls) a headless RDP‐at‐startup scheduled task and optional per‑user logon tasks.
.DESCRIPTION
When run without -Disable, this script:
• Ensures it’s elevated to Administrator.
• Stores (or reuses) RDP credentials in Windows Credential Manager.
• Downloads the FreeRDP client (if not already present) to .\bin\sdl3‑freerdp.exe.
• Registers a “HeadlessRdp_Bootstrap” task under \ScheduleAutoLogon\ that, at system startup,
spawns a background RDP session to $Host:$Port, waits, then tears it down—triggering
any per‑user AtLogOn tasks.
When run with -Disable, it cleans up: removes the scheduled tasks, deletes the credential entry,
and deletes the .\bin folder.
.PARAMETER Disable
Switch: if present, uninstalls the scheduled tasks, removes credentials and binaries, then exits.
.PARAMETER RunAsUser
The user name under which the task(s) will run. Defaults to $env:USERNAME.
.PARAMETER RunAsDomain
The domain (or machine name) for the RunAsUser. Defaults to $env:USERDOMAIN.
.NOTES
• Tested on Windows Server 2022 with PowerShell 5.1.
• Credential retrieval uses Win32 CredRead via P/Invoke.
• To tweak the RDP target, adjust the $Host and $Port constants below.
.EXAMPLE
# Install for the currently logged in user on the local machine
.\ScheduleAutoLogon.ps1
.EXAMPLE
# Uninstall everything
.\ScheduleAutoLogon.ps1 -Disable
#>
param(
[switch]$Disable,
[string]$RunAsUser = $env:USERNAME,
[string]$RunAsDomain = $env:USERDOMAIN
)
$ErrorActionPreference = 'Stop'
trap {
Write-Host ""; Write-Host ('FAILED: {0}' -f $_.Exception.Message) -ForegroundColor Red
Read-Host 'Press Enter to close'; exit 1
}
# ── Self‑elevate ──────────────────────────────────────────────────────────────
$cur = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not (New-Object Security.Principal.WindowsPrincipal $cur).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
$a = @('-NoProfile','-ExecutionPolicy','Bypass','-File',$PSCommandPath)
if ($Disable) { $a += '-Disable' }
$a += '-RunAsUser'; $a += $RunAsUser
$a += '-RunAsDomain'; $a += $RunAsDomain
Start-Process -FilePath "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -Verb RunAs -ArgumentList $a
exit
}
[Net.ServicePointManager]::SecurityProtocol = 3072 # TLS 1.2
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
# ── Constants ─────────────────────────────────────────────────────────────────
$taskUser = "$RunAsDomain\$RunAsUser"
$tsHost = "127.0.0.1"
$tsPort = 3089
$rootDir = Split-Path -Parent $PSCommandPath
$binDir = Join-Path $rootDir 'bin'
$freerdp = Join-Path $binDir 'sdl3-freerdp.exe'
$taskPath = '\ScheduleAutoLogon\'
$rdpTask = 'HeadlessRdp_Bootstrap'
$exeUrl = 'https://ci.freerdp.com/job/freerdp-nightly-windows/arch=win64,label=vs2017/lastSuccessfulBuild/artifact/install/bin/sdl3-freerdp.exe'
$credTag = 'TERMSRV/' + $tsHost + ':' + $tsPort
$logFile = "$binDir\freerdp.log"
# ── Uninstall path ───────────────────────────────────────────────────────────
function Uninstall-RdpStartup {
Write-Host 'Removing tasks and credentials ...' -ForegroundColor Yellow
Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false
schtasks /Delete /TN $rdpTask /F 2>$null | Out-Null
cmdkey /delete:$credTag 2>$null | Out-Null
if (Test-Path $binDir) { Remove-Item $binDir -Recurse -Force }
Write-Host 'Clean removal complete.' -ForegroundColor Green
Read-Host 'Press Enter to exit'; exit
}
if ($Disable) { Uninstall-RdpStartup }
# ── Ensure credential exists ─────────────────────────────────────────────────
if (-not (cmdkey /list | Select-String $credTag)) {
$pwd = Read-Host "Password for $taskUser (stored in Credential Manager)" -AsSecureString
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([
Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd))
cmdkey /generic:$credTag /user:$taskUser /pass:$plain | Out-Null
}
function Get-StoredGenericCredential {
param([string]$TargetName)
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class NativeCred {
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern bool CredRead(
string target, int type, int reservedFlag, out IntPtr credentialPtr);
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool CredFree(IntPtr buffer);
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct CREDENTIAL {
public UInt32 Flags;
public UInt32 Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public UInt32 CredentialBlobSize;
public IntPtr CredentialBlob;
public UInt32 Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}
}
"@
$ptr = [IntPtr]::Zero
if (-not [NativeCred]::CredRead($TargetName, 1, 0, [ref]$ptr)) {
throw "CredRead failed: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
$cred = [Runtime.InteropServices.Marshal]::PtrToStructure(
$ptr, [Type][NativeCred+CREDENTIAL])
$pwdBytes = New-Object byte[] $cred.CredentialBlobSize
[Runtime.InteropServices.Marshal]::Copy(
$cred.CredentialBlob, $pwdBytes, 0, $cred.CredentialBlobSize)
# Suppress the CredFree boolean:
[void][NativeCred]::CredFree($ptr)
# or: [NativeCred]::CredFree($ptr) | Out-Null
# blob is UTF‑16 little‑endian
$plain = [Text.Encoding]::Unicode.GetString($pwdBytes).TrimEnd([char]0)
return $plain
}
$plain = Get-StoredGenericCredential -TargetName $credTag
# ── Download FreeRDP if missing ──────────────────────────────────────────────
if (-not (Test-Path $freerdp)) {
Write-Host 'Downloading sdl3-freerdp.exe ...' -ForegroundColor Cyan
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir | Out-Null }
try { Invoke-WebRequest -Uri $exeUrl -OutFile $freerdp -UseBasicParsing }
catch { (New-Object System.Net.WebClient).DownloadFile($exeUrl,$freerdp) }
}
# ── Build FreeRDP argument list ──────────────────────────────────────────────
$rdpArgs = @(
'/v:' + $tsHost + ':' + $tsPort,
"/u:$RunAsUser",
"/d:$RunAsDomain",
"/p:$plain",
'/size:1280x800',
# '/log-level:TRACE',
'/cert:ignore',
'/sec:nla'
) -join ' '
# ── Build cmd.exe wrapper with WLog appender vars ────────────────────────────
# Wrap it with START, TIMEOUT, and TASKKILL
$cmd = '/c '
$cmd += 'set "WLOG_APPENDER=file" && '
$cmd += 'set "WLOG_FILEAPPENDER_TRUNCATE=TRUE" && '
$cmd += 'set "WLOG_FILEAPPENDER_OUTPUT_FILE_PATH=' + $binDir + '" && '
$cmd += 'set "WLOG_FILEAPPENDER_OUTPUT_FILE_NAME=freerdp.log" && '
# 1) Start in background (no /wait) so cmd.exe returns immediately
$cmd += 'start "" /B "' + $freerdp + '" ' + $rdpArgs + ' && '
# 2) Wait 60 seconds for the session to log on and your tasks to fire
$cmd += 'timeout /T 60 /NOBREAK >nul && '
# 3) Kill the client by image name
$cmd += 'taskkill /IM ' + [IO.Path]::GetFileName($freerdp) + ' /F'
# ── Remove existing tasks ────────────────────────────────────────────────────
Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue
# ── Register headless task ───────────────────────────────────────────────────
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $cmd -WorkingDirectory $binDir
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId $taskUser -LogonType S4U -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::FromMinutes(3))
Register-ScheduledTask -TaskName $rdpTask -TaskPath $taskPath -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
# ── Finished ────────────────────────────────────────────────────────────────
Write-Host ''
Write-Host 'SUCCESS - hidden RDP session will spawn at boot; console remains locked.' -ForegroundColor Green
Read-Host 'Press Enter to exit'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment