feat(local-windows-rdp): local Windows RDP using coder desktop (#119)

Introduces coder module: local-windows-rdp
- Creates a coder app that can launch local rdp with auto-login using
coder-desktop
- Runs a PowerShell script inside of the VM setting RDP permissions, and
sets Username and Password inside of VM


### Testing
- [x] AWS
- [x] GCP
- [ ] Azure

---------

Co-authored-by: Atif Ali <atif@coder.com>
This commit is contained in:
DevCats 2025-06-23 08:03:44 -05:00 committed by GitHub
parent 7a2b1ac76d
commit 77392cc146
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 459 additions and 0 deletions

View File

@ -0,0 +1,74 @@
---
display_name: Windows RDP Desktop
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
icon: ../../../../.icons/desktop.svg
maintainer_github: coder
verified: true
supported_os: [windows]
tags: [rdp, windows, desktop, remote]
---
# Windows RDP Desktop
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
> **Note**: [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
agent_name = coder_agent.main.name
}
```
## Features
- ✅ **Standalone Solution**: Automatically configures RDP on Windows workspaces
- ✅ **One-click Access**: Launch RDP sessions directly through Coder Desktop
- ✅ **No Port Forwarding**: Uses Coder Desktop URI handling
- ✅ **Auto-configuration**: Sets up Windows firewall, services, and authentication
- ✅ **Secure**: Configurable credentials with sensitive variable handling
- ✅ **Customizable**: Display name, credentials, and UI ordering options
## What This Module Does
1. **Enables RDP** on the Windows workspace
2. **Sets the administrator password** for RDP authentication
3. **Configures Windows Firewall** to allow RDP connections
4. **Starts RDP services** automatically
5. **Creates a Coder Desktop button** for one-click access
## Examples
### Basic Usage
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
agent_name = coder_agent.main.name
}
```
### Custom display name
Specify a custom display name for the `coder_app` button:
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.windows.id
agent_name = "windows"
display_name = "Windows Desktop"
order = 1
}
```

View File

@ -0,0 +1,120 @@
# PowerShell script to configure RDP for Coder Desktop access
# This script enables RDP, sets the admin password, and configures necessary settings
Write-Output "[Coder RDP Setup] Starting RDP configuration..."
# Function to set the administrator password
function Set-AdminPassword {
param (
[string]$adminUsername,
[string]$adminPassword
)
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername"
try {
# Convert password to secure string
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force
# Set the password for the user
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword
# Enable the user account (in case it's disabled)
Get-LocalUser -Name $adminUsername | Enable-LocalUser
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername"
} catch {
Write-Error "[Coder RDP Setup] Failed to set password: $_"
exit 1
}
}
# Function to enable and configure RDP
function Enable-RDP {
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..."
try {
# Enable RDP
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force
# Disable Network Level Authentication (NLA) for easier access
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force
# Set security layer to RDP Security Layer
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force
Write-Output "[Coder RDP Setup] RDP enabled successfully"
} catch {
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_"
exit 1
}
}
# Function to configure Windows Firewall for RDP
function Configure-Firewall {
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..."
try {
# Enable RDP firewall rules
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
# If the above fails, try alternative method
if ($LASTEXITCODE -ne 0) {
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
}
Write-Output "[Coder RDP Setup] Firewall configured successfully"
} catch {
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_"
# Continue anyway as RDP might still work
}
}
# Function to ensure RDP service is running
function Start-RDPService {
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..."
try {
# Start the Terminal Services
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service -Name "TermService" -ErrorAction SilentlyContinue
# Start Remote Desktop Services UserMode Port Redirector
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue
Write-Output "[Coder RDP Setup] RDP services started successfully"
} catch {
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_"
# Continue anyway
}
}
# Main execution
try {
# Template variables from Terraform
$username = "${username}"
$password = "${password}"
# Validate inputs
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
Write-Error "[Coder RDP Setup] Username or password is empty"
exit 1
}
# Execute configuration steps
Set-AdminPassword -adminUsername $username -adminPassword $password
Enable-RDP
Configure-Firewall
Start-RDPService
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!"
Write-Output "[Coder RDP Setup] You can now connect using:"
Write-Output " Username: $username"
Write-Output " Password: [hidden]"
Write-Output " Port: 3389 (default)"
} catch {
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_"
exit 1
}

View File

@ -0,0 +1,184 @@
import { describe, expect, it } from "bun:test";
import {
type TerraformState,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
type TestVariables = Readonly<{
agent_id: string;
agent_name: string;
username?: string;
password?: string;
display_name?: string;
order?: number;
}>;
function findRdpApp(state: TerraformState) {
for (const resource of state.resources) {
const isRdpAppResource =
resource.type === "coder_app" && resource.name === "rdp_desktop";
if (!isRdpAppResource) {
continue;
}
for (const instance of resource.instances) {
if (instance.attributes.slug === "rdp-desktop") {
return instance.attributes;
}
}
}
return null;
}
function findRdpScript(state: TerraformState) {
for (const resource of state.resources) {
const isRdpScriptResource =
resource.type === "coder_script" && resource.name === "rdp_setup";
if (!isRdpScriptResource) {
continue;
}
for (const instance of resource.instances) {
if (instance.attributes.display_name === "Configure RDP") {
return instance.attributes;
}
}
}
return null;
}
describe("local-windows-rdp", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "test-agent",
});
it("should create RDP app with default values", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const app = findRdpApp(state);
// Verify the app was created
expect(app).not.toBeNull();
expect(app?.slug).toBe("rdp-desktop");
expect(app?.display_name).toBe("RDP Desktop");
expect(app?.icon).toBe("/icon/desktop.svg");
expect(app?.external).toBe(true);
// Verify the URI format
expect(app?.url).toStartWith("coder://");
expect(app?.url).toContain("/v0/open/ws/");
expect(app?.url).toContain("/agent/main/rdp");
expect(app?.url).toContain("username=Administrator");
expect(app?.url).toContain("password=coderRDP!");
});
it("should create RDP configuration script", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const script = findRdpScript(state);
// Verify the script was created
expect(script).not.toBeNull();
expect(script?.display_name).toBe("Configure RDP");
expect(script?.icon).toBe("/icon/desktop.svg");
expect(script?.run_on_start).toBe(true);
expect(script?.run_on_stop).toBe(false);
// Verify the script contains PowerShell configuration
expect(script?.script).toContain("Set-AdminPassword");
expect(script?.script).toContain("Enable-RDP");
expect(script?.script).toContain("Configure-Firewall");
expect(script?.script).toContain("Start-RDPService");
});
it("should create RDP app with custom values", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "custom-agent-id",
agent_name: "windows-agent",
username: "CustomUser",
password: "CustomPass123!",
display_name: "Custom RDP",
order: 5,
});
const app = findRdpApp(state);
// Verify custom values
expect(app?.display_name).toBe("Custom RDP");
expect(app?.order).toBe(5);
// Verify custom credentials in URI
expect(app?.url).toContain("/agent/windows-agent/rdp");
expect(app?.url).toContain("username=CustomUser");
expect(app?.url).toContain("password=CustomPass123!");
});
it("should pass custom credentials to PowerShell script", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
username: "TestAdmin",
password: "TestPassword123!",
});
const script = findRdpScript(state);
// Verify custom credentials are in the script
expect(script?.script).toContain('$username = "TestAdmin"');
expect(script?.script).toContain('$password = "TestPassword123!"');
});
it("should handle sensitive password variable", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
password: "SensitivePass123!",
});
const app = findRdpApp(state);
// Verify password is included in URI even when sensitive
expect(app?.url).toContain("password=SensitivePass123!");
});
it("should use correct default agent name", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const app = findRdpApp(state);
expect(app?.url).toContain("/agent/main/rdp");
});
it("should construct proper Coder URI format", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "test-agent",
username: "TestUser",
password: "TestPass",
});
const app = findRdpApp(state);
// Verify complete URI structure
expect(app?.url).toMatch(
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/,
);
});
});

View File

@ -0,0 +1,81 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "agent_name" {
type = string
description = "The name of the Coder agent."
}
variable "username" {
type = string
description = "The username for RDP authentication."
default = "Administrator"
}
variable "password" {
type = string
description = "The password for RDP authentication."
default = "coderRDP!"
sensitive = true
}
variable "display_name" {
type = string
description = "The display name for the RDP app button."
default = "RDP Desktop"
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
locals {
# Extract server name from workspace access URL
server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0]
}
data "coder_workspace" "me" {}
resource "coder_script" "rdp_setup" {
agent_id = var.agent_id
display_name = "Configure RDP"
icon = "/icon/desktop.svg"
script = templatefile("${path.module}/configure-rdp.ps1", {
username = var.username
password = var.password
})
run_on_start = true
}
resource "coder_app" "rdp_desktop" {
agent_id = var.agent_id
slug = "rdp-desktop"
display_name = var.display_name
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}"
icon = "/icon/desktop.svg"
external = true
order = var.order
group = var.group
}