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:
parent
7a2b1ac76d
commit
77392cc146
74
registry/coder/modules/local-windows-rdp/README.md
Normal file
74
registry/coder/modules/local-windows-rdp/README.md
Normal 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
|
||||||
|
}
|
||||||
|
```
|
||||||
120
registry/coder/modules/local-windows-rdp/configure-rdp.ps1
Normal file
120
registry/coder/modules/local-windows-rdp/configure-rdp.ps1
Normal 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
|
||||||
|
}
|
||||||
184
registry/coder/modules/local-windows-rdp/main.test.ts
Normal file
184
registry/coder/modules/local-windows-rdp/main.test.ts
Normal 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$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
81
registry/coder/modules/local-windows-rdp/main.tf
Normal file
81
registry/coder/modules/local-windows-rdp/main.tf
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user