8000 feat(local-windows-rdp): local Windows RDP using coder desktop (#119) · coder/registry@77392cc · GitHub
[go: up one dir, main page]

Skip to content

Commit 77392cc

Browse files
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>
1 parent 7a2b1ac commit 77392cc

File tree

4 files changed

+459
-0
lines changed

4 files changed

+459
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
display_name: Windows RDP Desktop
3+
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
4+
icon: ../../../../.icons/desktop.svg
5+
maintainer_github: coder
6+
verified: true
7+
supported_os: [windows]
8+
tags: [rdp, windows, desktop, remote]
9+
---
10+
11+
# Windows RDP Desktop
12+
13+
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.
14+
15+
> **Note**: [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
16+
17+
```tf
18+
module "rdp_desktop" {
19+
count = data.coder_workspace.me.start_count
20+
source = "registry.coder.com/coder/local-windows-rdp/coder"
21+
version = "1.0.0"
22+
agent_id = coder_agent.main.id
23+
agent_name = coder_agent.main.name
24+
}
25+
```
26+
27+
## Features
28+
29+
-**Standalone Solution**: Automatically configures RDP on Windows workspaces
30+
-**One-click Access**: Launch RDP sessions directly through Coder Desktop
31+
-**No Port Forwarding**: Uses Coder Desktop URI handling
32+
-**Auto-configuration**: Sets up Windows firewall, services, and authentication
33+
-**Secure**: Configurable credentials with sensitive variable handling
34+
-**Customizable**: Display name, credentials, and UI ordering options
35+
36+
## What This Module Does
37+
38+
1. **Enables RDP** on the Windows workspace
39+
2. **Sets the administrator password** for RDP authentication
40+
3. **Configures Windows Firewall** to allow RDP connections
41+
4. **Starts RDP services** automatically
42+
5. **Creates a Coder Desktop button** for one-click access
43+
44+
## Examples
45+
46+
### Basic Usage
47+
48+
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
49+
50+
```tf
51+
module "rdp_desktop" {
52+
count = data.coder_workspace.me.start_count
53+
source = "registry.coder.com/coder/local-windows-rdp/coder"
54+
version = "1.0.0"
55+
agent_id = coder_agent.main.id
56+
agent_name = coder_agent.main.name
57+
}
58+
```
59+
60+
### Custom display name
61+
62+
Specify a custom display name for the `coder_app` button:
63+
64+
```tf
65+
module "rdp_desktop" {
66+
count = data.coder_workspace.me.start_count
67+
source = "registry.coder.com/coder/local-windows-rdp/coder"
68+
version = "1.0.0"
69+
agent_id = coder_agent.windows.id
70+
agent_name = "windows"
71+
display_name = "Windows Desktop"
72+
order = 1
73+
}
74+
```
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# PowerShell script to configure RDP for Coder Desktop access
2+
# This script enables RDP, sets the admin password, and configures necessary settings
3+
4+
Write-Output "[Coder RDP Setup] Starting RDP configuration..."
5+
6+
# Function to set the administrator password
7+
function Set-AdminPassword {
8+
param (
9+
[string]$adminUsername,
10+
[string]$adminPassword
11+
)
12+
13+
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername"
14+
15+
try {
16+
# Convert password to secure string
17+
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force
18+
19+
# Set the password for the user
20+
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword
21+
22+
# Enable the user account (in case it's disabled)
23+
Get-LocalUser -Name $adminUsername | Enable-LocalUser
24+
25+
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername"
26+
} catch {
27+
Write-Error "[Coder RDP Setup] Failed to set password: $_"
28+
exit 1
29+
}
30+
}
31+
32+
# Function to enable and configure RDP
33+
function Enable-RDP {
34+
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..."
35+
36+
try {
37+
# Enable RDP
38+
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force
39+
40+
# Disable Network Level Authentication (NLA) for easier access
41+
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force
42+
43+
# Set security layer to RDP Security Layer
44+
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force
45+
46+
Write-Output "[Coder RDP Setup] RDP enabled successfully"
47+
} catch {
48+
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_"
49+
exit 1
50+
}
51+
}
52+
53+
# Function to configure Windows Firewall for RDP
54+
function Configure-Firewall {
55+
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..."
56+
57+
try {
58+
# Enable RDP firewall rules
59+
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
60+
61+
# If the above fails, try alternative method
62+
if ($LASTEXITCODE -ne 0) {
63+
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
64+
}
65+
66+
Write-Output "[Coder RDP Setup] Firewall configured successfully"
67+
} catch {
68+
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_"
69+
# Continue anyway as RDP might still work
70+
}
71+
}
72+
73+
# Function to ensure RDP service is running
74+
function Start-RDPService {
75+
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..."
76+
77+
try {
78+
# Start the Terminal Services
79+
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue
80+
Start-Service -Name "TermService" -ErrorAction SilentlyContinue
81+
82+
# Start Remote Desktop Services UserMode Port Redirector
83+
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue
84+
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue
85+
86+
Write-Output "[Coder RDP Setup] RDP services started successfully"
87+
} catch {
88+
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_"
89+
# Continue anyway
90+
}
91+
}
92+
93+
# Main execution
94+
try {
95+
# Template variables from Terraform
96+
$username = "${username}"
97+
$password = "${password}"
98+
99+
# Validate inputs
100+
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
101+
Write-Error "[Coder RDP Setup] Username or password is empty"
102+
exit 1
103+
}
104+
105+
# Execute configuration steps
106+
Set-AdminPassword -adminUsername $username -adminPassword $password
107+
Enable-RDP
108+
Configure-Firewall
109+
Start-RDPService
110+
111+
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!"
112+
Write-Output "[Coder RDP Setup] You can now connect using:"
113+
Write-Output " Username: $username"
114+
Write-Output " Password: [hidden]"
115+
Write-Output " Port: 3389 (default)"
116+
117+
} catch {
118+
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_"
119+
exit 1
120+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, expect, it } from "bun:test";
2+
import {
3+
type TerraformState,
4+
runTerraformApply,
5+
runTerraformInit,
6+
testRequiredVariables,
7+
} from "~test";
8+
9+
type TestVariables = Readonly<{
10+
agent_id: string;
11+
agent_name: string;
12+
username?: string;
13+
password?: string;
14+
display_name?: string;
15+
order?: number;
16+
}>;
17+
18+
function findRdpApp(state: TerraformState) {
19+
for (const resource of state.resources) {
20+
const isRdpAppResource =
21+
resource.type === "coder_app" && resource.name === "rdp_desktop";
22+
23+
if (!isRdpAppResource) {
24+
continue;
25+
}
26+
27+
for (const instance of resource.instances) {
28+
if (instance.attributes.slug === "rdp-desktop") {
29+
return instance.attributes;
30+
}
31+
}
32+
}
33+
34+
return null;
35+
}
36+
37+
function findRdpScript(state: TerraformState) {
38+
for (const resource of state.resources) {
39+
const isRdpScriptResource =
40+
resource.type === "coder_script" && resource.name === "rdp_setup";
41+
42+
if (!isRdpScriptResource) {
43+
continue;
44+
}
45+
46+
for (const instance of resource.instances) {
47+
if (instance.attributes.display_name === "Configure RDP") {
48+
return instance.attributes;
49+
}
50+
}
51+
}
52+
53+
return null;
54+
}
55+
56+
describe("local-windows-rdp", async () => {
57+
await runTerraformInit(import.meta.dir);
58+
59+
testRequiredVariables<TestVariables>(import.meta.dir, {
60+
agent_id: "test-agent-id",
61+
agent_name: "test-agent",
62+
});
63+
64+
it("should create RDP app with default values", async () => {
65+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
66+
agent_id: "test-agent-id",
67+
agent_name: "main",
68+
});
69+
70+
const app = findRdpApp(state);
71+
72+
// Verify the app was created
73+
expect(app).not.toBeNull();
74+
expect(app?.slug).toBe("rdp-desktop");
75+
expect(app?.display_name).toBe("RDP Desktop");
76+
expect(app?.icon).toBe("/icon/desktop.svg");
77+
expect(app?.external).toBe(true);
78+
79+
// Verify the URI format
80+
expect(app?.url).toStartWith("coder://");
81+
expect(app?.url).toContain("/v0/open/ws/");
82+
expect(app?.url).toContain("/agent/main/rdp");
83+
expect(app?.url).toContain("username=Administrator");
84+
expect(app?.url).toContain("password=coderRDP!");
85+
});
86+
87+
it("should create RDP configuration script", async () => {
88+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
89+
agent_id: "test-agent-id",
90+
agent_name: "main",
91+
});
92+
93+
const script = findRdpScript(state);
94+
95+
// Verify the script was created
96+
expect(script).not.toBeNull();
97+
expect(script?.display_name).toBe("Configure RDP");
98+
expect(script?.icon).toBe("/icon/desktop.svg");
99+
expect(script?.run_on_start).toBe(true);
100+
expect(script?.run_on_stop).toBe(false);
101+
102+
// Verify the script contains PowerShell configuration
103+
expect(script?.script).toContain("Set-AdminPassword");
104+
expect(script?.script).toContain("Enable-RDP");
105+
expect(script?.script).toContain("Configure-Firewall");
106+
expect(script?.script).toContain("Start-RDPService");
107+
});
108+
109+
it("should create RDP app with custom values", async () => {
110+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
111+
agent_id: "custom-agent-id",
112+
agent_name: "windows-agent",
113+
username: "CustomUser",
114+
password: "CustomPass123!",
115+
display_name: "Custom RDP",
116+
order: 5,
117+
});
118+
119+
const app = findRdpApp(state);
120+
121+
// Verify custom values
122+
expect(app?.display_name).toBe("Custom RDP");
123+
expect(app?.order).toBe(5);
124+
125+
// Verify custom credentials in URI
126+
expect(app?.url).toContain("/agent/windows-agent/rdp");
127+
expect(app?.url).toContain("username=CustomUser");
128+
expect(app?.url).toContain("password=CustomPass123!");
129+
});
130+
131+
it("should pass custom credentials to PowerShell script", async () => {
132+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
133+
agent_id: "test-agent-id",
134+
agent_name: "main",
135+
username: "TestAdmin",
136+
password: "TestPassword123!",
137+
});
138+
139+
const script = findRdpScript(state);
140+
141+
// Verify custom credentials are in the script
142+
expect(script?.script).toContain('$username = "TestAdmin"');
143+
expect(script?.script).toContain('$password = "TestPassword123!"');
144+
});
145+
146+
it("should handle sensitive password variable", async () => {
147+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
148+
agent_id: "test-agent-id",
149+
agent_name: "main",
150+
password: "SensitivePass123!",
151+
});
152+
153+
const app = findRdpApp(state);
154+
155+
// Verify password is included in URI even when sensitive
156+
expect(app?.url).toContain("password=SensitivePass123!");
157+
});
158+
159+
it("should use correct default agent name", async () => {
160+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
161+
agent_id: "test-agent-id",
162+
agent_name: "main",
163+
});
164+
165+
const app = findRdpApp(state);
166+
expect(app?.url).toContain("/agent/main/rdp");
167+
});
168+
169+
it("should construct proper Coder URI format", async () => {
170+
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
171+
agent_id: "test-agent-id",
172+
agent_name: "test-agent",
173+
username: "TestUser",
174+
password: "TestPass",
175+
});
176+
177+
const app = findRdpApp(state);
178+
179+
// Verify complete URI structure
180+
expect(app?.url).toMatch(
181+
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/,
182+
);
183+
});
184+
});

0 commit comments

Comments
 (0)
0