Creating a Reproducible Remote Desktop with Terraform
Desk Dance
© Photo by Nat Weerawong on Unsplash
The Terraform orchestration tool lets you create a remote desktop configuration that you can easily bring up and tear down, which could save you money and add flexibility to your environment.
The concept of a remote desktop has been around for a long time. The idea is to store a regular desktop configuration on some remote server and let the user connect to it using a remote desktop tool like RDP or VNC. One common approach is to set up a physical server with beefy hardware and then provide a connection to it from a thin client. This option has many downsides, such as the possibility of hardware issues or electrical outages.
An alternative approach is to treat your remote desktop the same way you treat cloud resources – that is, building it on demand and using it only when you actually need it. This solution is certainly cheaper than running RDP 24/7. Plus, the fact that the desktop only exists when you need it also reduces the possibility of a cyberattack. An added advantage is that building on demand lets you scale vertically within minutes.
I wanted a remote desktop that could be quickly created and destroyed, but with a state that persisted between the sessions. Some more specific requirements included:
- It must be flexible in terms of location and server size, to the greatest extent allowed by the cloud service provider.
- The desktop state must be captured in an image taken at shutdown, so the desktop will appear as you left it the next time this server is started.
- User data should persist outside the server on a connected hard drive. This user data will be encrypted at rest.
- There should be a bastion server to connect to the remote desktop – this way, the remote desktop server is not exposed to the Internet directly.
As an experiment, I will use a Raspberry Pi – of course, you could use another system, but I love the idea of having the setup on my Raspberry Pi. The latest Raspberry Pi OS (formerly Raspbian) should do fine; of course, feel free to adapt the commands accordingly. I will use Hetzner for hosting because it is inexpensive and they have a data center located close to me (Finland). Hetzner also supports automation via Terraform.
The Terraform declarative orchestration tool lets you declare what resources you'd like to create (i.e., "I want to have a server connected to the network, with storage, etc."). You provide the description, and Terraform figures out how to create those resources with a provider of your choice.
If you change your list of requirements and run it again, Terraform will compare the current state of the infrastructure with your desired result and only create additional resources (or modify existing resources) that are needed.
This example shows how to put Terraform to work on a simple and practical solution. Of course, you could do something similar using a Bash scripts, but why not make use of a powerful and versatile configuration tool that will be easier to adapt if you migrate to a different provider?
Installing
Start by installing Terraform, as shown in Listing 1. (See the Terraform website [1] for more on installing Terraform with different operating systems.)
Listing 1
Installing Terraform
# Install prerequisites sudo apt-get update sudo apt-get install -y gnupg curl jq # Add HashiCorp GPG key curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg # Add the official HashiCorp repo echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com \ $(. /etc/os-release && echo $VERSION_CODENAME) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list # Install the latest Terraform sudo apt-get update sudo apt-get install -y terraform # Verify that Terraform has been installed terraform -version
To automate the setup, you'll need an API token from Hetzner. Create your own account on hetzner.com. Once everything is set up, go to the Hetzner Cloud Console [2].
Your console contains all your resources (cloud VMs, firewalls, networks, and so forth) split into "projects." Create a new project and open it. On the left side, you'll see a list of your resources (Servers, Volumes, Floating IPs). At the bottom of the list is a Security button. Click the Security button, and you will see SSH keys, S3 credentials, and other options. Click API tokens, and you'll arrive at a page where you can click Generate API token (Figure 1). Enter an appropriate description and make sure you select Read & Write under permissions. Copy the key.
Initial Setup
The next step is to start setting things up. Create a directory called office. All the work will happen in this directory. Start with the basic Terraform and Hetzner setup. Create a providers.tf file with the content in Listing 2.
Listing 2
providers.tf
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.44" # Or latest stable
}
}
backend "local" {
path = "./out/terraform.tfstate"
}
}
provider "hcloud" {
token = var.hcloud_token
}
Listing 2 tells Terraform that it should use Hetzner as a provider. You will also want to store the Terraform state in the out folder in a terraform.tfstate file. The state file basically stores information about resources that Terraform creates. There is a bit more to it, but you can refer to the Terraform documentation [3] for further info.
Create a variables.tf file, where you can store variables used in the configuration (Listing 3).
Listing 3
variables.tf
variable "hcloud_token" {
type = string
description = "Hetzner API token"
sensitive = true # This prevents it from being output in the terminal
}
variable "image_id" {
type = string
default = ""
description = "Optional snapshot ID for virtual office server"
}
variable "bastion_server_type" {
type = string
default = "cx23" # Note that we can provide default reasonable value. This can be overridden consequently.
description = "Hetzner server type for bastion (e.g., cpx11)"
}
variable "office_server_type" {
type = string
default = "cx23"
description = "Hetzner server type for office (e.g., cpx41)"
}
variable "location" {
type = string
default = "hel1"
description = "Hetzner location slug (e.g., hel1)"
}
variable "up" {
type = bool
default = true
description = "Whether to create servers or destroy them."
}
Don't put your token in a .tf file (in case this code ends up in a repository). Instead, create an office.tfvars file and put your token there (Listing 4).
Listing 4
office.tfvars
hcloud_token="<your-token>"
Now it is time to test the setup. You aren't done yet, but it is a good idea to verify that everything is OK. Make sure you are still in the office directory and run the commands in Listing 5.
Listing 5
Testing Setup
terraform init -var-file=office.tfvars terraform plan -var-file=office.tfvars
After the first command, you should see the message "Terraform has been successfully initialized". During this init process, Terraform downloads the extra files that it needs to be able to work for a given configuration (listed in providers.tf).
After the second command, you should see "No changes. Your infrastructure matches the configuration." In this case, the configuration is pretty much empty – you haven't defined any resources yet, thus the infrastructure (nonexistent) matches the empty setup.
I should mention that you can name your files differently as long as they have the .tf extension. You can also move resources between them. The file structure described in this article works for me, but you could even put it all into one main.tf if it makes more sense or is easier to maintain. Of course, the .tfvars file still has to be separate.
SSH Keys
You can ask Terraform to generate your SSH keys, to avoid having to manually do this; after all, this is an automated setup. Before I show how to do that, I will take a quick detour to describe how Terraform creates things. To put it very simply, you just need to list what you want to create – an SSH key, a server, a network. Each item you create is called a resource and each resource has a type (key/server/etc.), a name (bastion server, personal ssh key, etc.) and a number of parameters. For a server, the parameters might include the type and location. For an SSH key, a parameter might be the algorithm used to generate it. For a local file, a parameter might be the content that you want to put in it.
When you run the plan command, Terraform reviews the previous infrastructure it has created (or none, if you are running it for the first time), and compares that to all the resources you've described. It can then deduce:
- Which new resources need to be created
- Which resources have to be updated (e.g., a cloud disk can be "increased" in size)
- Which resources have to be replaced (e.g., if you can decide to move your server to another data center, an old server would be destroyed and new one created)
- Which resources have to be deleted.
Terraform's goal is to make your real infrastructure match all the resource descriptions that you list in your .tf files.
After this review, Terraform will print out its plan and quit. In order to actually apply changes, you will need to run the apply command. Terraform will then undertake the same planning process it used with the plan command, but in the end, it will ask you to approve your changes manually.
Listing 6 shows the ssh.tf file. I have commented on every resource you need.
Listing 6
ssh.tf
# This is the actual SSH key, consisting of private key and public key
resource "tls_private_key" "ssh_key" {
algorithm = "ED25519"
}
# This resource persists the generated SSH private key into a local file
resource "local_file" "private_key" {
content = tls_private_key.ssh_key.private_key_openssh
filename = "${path.module}/out/ssh/id_ed25519"
file_permission = "0600"
}
# This resource persists the generated SSH public key into a local file
resource "local_file" "public_key" {
content = tls_private_key.ssh_key.public_key_openssh
filename = "${path.module}/out/ssh/id_ed25519.pub"
}
# This part is very important. When you create a server on Hetzner, you can specify to use the SSH key instead of password, but you need to upload your key first.
# This resource represents an uploaded key -- when created by Terraform, the actual key gets persisted on Hetzner's side.
resource "hcloud_ssh_key" "default" {
name = "terraform-key"
public_key = tls_private_key.ssh_key.public_key_openssh
}
# This resource represents a local file with the SSH config, so that you can use it for SSH-ing into the servers.
# Notice that the bastion host refers to an IP address from somewhere else -- I will introduce that in a moment.
# As for the "office" host, it has a pre-defined IP that I'll configure elsewhere.
# Don't mind the "count" part -- it will be explained later; but generally, this means that we do NOT create this resource
# IF "up" variable is false or not defined.
resource "local_file" "ssh_config" {
count = var.up ? 1 : 0
content = <<-EOT
Host bastion
HostName ${hcloud_server.bastion[0].ipv4_address}
User root
IdentityFile ${local_file.private_key.filename}
IdentitiesOnly yes
Host office
HostName 10.0.1.10
User root
IdentityFile ${local_file.private_key.filename}
IdentitiesOnly yes
ProxyJump bastion
EOT
filename = "${path.module}/out/ssh/config"
}
You are not done yet, though, so do not run Terraform yet. It is time to set up the network and servers.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Linux Foundation Report Indicates AI Driving Tech Hiring
Within growing security and skills gaps, AI has been found to be a positive driving force behind tech hiring trends in Europe.
-
United Nations Open Source Portal Goes Live
A new open source portal seeks to coordinate and scale open source efforts across the United Nations system.
-
KDE Linux Drops AUR
KDE Linux developers have dropped the Arch User Repository from the build pipeline due to security concerns; other distributions should consider doing the same.
-
California May Exempt Linux from Its Age-Verification Law
After backlash from the Linux community, California may be backing off on its promise to force all operating systems to verify age, but one platform may still have to comply.
-
Another Logic Bug Found in Linux Kernel
Qualys has discovered a vulnerability in the Linux kernel that can be used to elevate standard user privileges.
-
Ubuntu Core 26 Offers Game-Changing Enterprise Features
Ubuntu Core 26 could be a game-changer for organizations looking for increased security and reliability.
-
AI Flooding the Linux Kernel Security Mailing List
AI is giving Linus Torvalds a headache, but not in the way you might think.
-
Top Priorities for Open Source Pros Seeking a New Job
Professional fulfillment tops the list, according to LPI report.
-
Container-Based Fedora Hummingbird Designed for Agent-First Builders
Fedora Hummingbird brings the same approach to the host OS as it does to containers to level up security.
-
Linux kernel Developers Considering a Kill Switch
With the rise of Linux vulnerabilities, the kernel developers are now considering adding a component that could help temporarily mitigate against them… in the form of a kill switch.
