Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions registry/coder/modules/local-windows-rdp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
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
tags: [rdp, windows, desktop, remote]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tags: [rdp, windows, desktop, remote]
coder_desktop: true
supported_os: [windows]
tags: [rdp, windows, desktop, remote]

See coder/registry-server#204 and coder/registry-server#199. We still need to implement these in the backend.

@Parkreiner, is it fine if we add these extra metadata entries even though they are yet handled in the registry server?

---

# 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. It provides a complete, standalone solution for RDP access without requiring manual configuration or port forwarding.

```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

## Requirements

- **Coder Desktop**: Must be installed on the client machine ([Download here](https://coder.com/docs/user-guides/desktop))

## 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
}
```
120 changes: 120 additions & 0 deletions registry/coder/modules/local-windows-rdp/configure-rdp.ps1
Original file line number Diff line number Diff line change
@@ -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
}
184 changes: 184 additions & 0 deletions registry/coder/modules/local-windows-rdp/main.test.ts
Original file line number Diff line number Diff line change
@@ -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$/,
);
});
});
Loading