Build a Secure Web Browser Remote Desktop Gateway on a Raspberry Pi

Goal: Let’s set up a clientless, web-based remote desktop gateway server to securely connect over HTTPS through a restrictive firewall using just a web browser, a spare Raspberry Pi 3, Docker, and Cloudflare.

Let’s say I’m in a Starbucks or the airport (or both) and I want to connect to my Windows (or OSX or Linux) machine to do a bit of work. Maybe I don’t want to take my primary computer with me on vacation or wait for hours when Customs randomly selects me (again); maybe I’ll just take a fresh Chromebook. Behind a restrictive firewall all we have is port 80 and port 443 (no VNC or RDP allowed), so let’s make remote desktop magic happen with zero spend on software.

Enter Apache Guacamole: a clientless remote desktop gateway that can be accessed in a browser.


Requirements

  • A target machine (running RDP or VNC)
  • A Raspberry Pi 3+
  • Apache Guacamole
  • Docker
  • Cloudflare
  • A modern web browser

The rest of the software is open-source.

Part 1: Setup the basic infrastructure

I’ll remotely connect to a Windows 10 machine running in a VMWare on my LAN. The VM is bridged to the physical network (no NAT forwarding required). There is a gateway router acting as DHCP, and there is a fresh Raspberry Pi 3+ on the LAN. The plan is to set static local IPs for the VMWare instance and RPi, install Docker, install Apache Guacamole, setup Cloudflare, and install Let’s Encrypt on the RPi.

1. Enable RDP on Windows 10

I’ve connected with both VNC and RDP, and RDP is much faster and smoother. On Windows 10 enable Remote Desktop like so. First, open the Remote Desktop Settings.

Open Remote Desktop Settings
Open Remote Desktop Settings

Select “Enable Remote Desktop”. Then click “Select users than can remotely access this PC”.

Enable RDP on Windows 10
Enable RDP on Windows 10

Administrators can remotely connect. If you want to specify an ordinary user, you can search for a user in “Advanced”.

Select RDP users
Select RDP users

2. Set static IPs

Power on the Raspberry Pi and we can set two static IPs at once. Using the stock Netgear router firmware as an example, find the address reservation screen in advanced settings. Assign an IP on the local subnet to the target machine. Assign another one to the RPi. I just used whatever the DHCP assigned them, and from now on they will not change.

Static IP reservations at the gateway router
Static IP reservations at the gateway router

3. Test the RDP connection

One a different machine on the same LAN, let’s test the RDP connection to the target machine. This is a good step to confirm what your username is when connecting. Is the user name capitalized? Is the machine name required as whoami suggests is the username? Experiment here. Use the static IP set in the previous step. If successful, then you will be logged out of the target machine and a new window will open showing the open desktop.

Test the RDP connection
Test the RDP connection

4. Install Docker on the Raspberry Pi 3

Wanting to physically isolate my externally-facing services, and then isolate them from each other, I like putting services in Docker containers on Raspberry Pis. For instance, I put Pi-hole on an RPi. Here is a quick start to installing Docker.

5. Install Apache Guacamole in Docker

The official Apache Guacamole Docker image doesn’t work on the Raspberry Pi. It’s also cumbersome to set up normally. Fortunately, I found a custom Docker image that is self-contained and works on ARM processors. Here is the Docker command. This will name the container guacamole and cause it to restart on boot and failure. It will persist configuration files to /var/guacamole on the RPi as well as bind port 8080, but you can set any port like -p 9000:8080.

Wait about three minutes as the internal Postgres DB is built and Tomcat starts up. Verify the container is running with docker ps.

6. Configure Guacamole for RDP

Login to Guacamole at the static IP you assigned it and port 8080 with the default username guacadmin and password guacadmin.

It has been noted that there is a bug with this image. If you are having problems logging in to Guacamole with the default credentials (guacadmin:guacadmin), then stop the container and start it again with docker container restart guacamole.
Login to Guacamole on the RPi
Login to Guacamole on the RPi

It’s a good idea to change your default password and enact some account restrictions. It’s even better to make another account that is not an admin and use that for remote desktop purposes.

Change default password and account restrictions
Change default password and account restrictions

Under Settings > Connections create a new connection.

Create a new RDP connection
Create a new RDP connection

Set the name of the connection and the protocol. In this case, it is RDP. You can also set connection limits for security.

Set the connection name and RDP protocol
Set the connection name and RDP protocol

Set the “Network” to the static IP of the target Windows machine. The RDP port is 3389. Set the username and password you used when testing the RDP connection previously. The security mode can either be “Any” or NLA. TLS and RDP are not working at this time. Be sure to select “Ignore server certificate” because it is a self-signed cert that cannot be verified. Finally, make sure all the fields in Remote Desktop Gateway are empty. There are many, many options that can be set to customize your remote desktop interaction. For now, I’ve left them all off.

Enter the RDP credentials in Guacamole
Enter the RDP credentials in Guacamole

7. Test Guacamole

Let’s open a browser on the host machine and connect through Guacamole to the target Win 10 machine in a VMWare instance. If successful, you will be logged out of the target machine and be presented with an open desktop in the web browser.

Ready to test Guacamole with a browser
Ready to establish a remote desktop connection
Successful connection to the remote desktop
Successful connection to the remote desktop

8.Forward ports 80 and 443 to the Raspberry Pi

First, let’s expose Guacamole (for now) on standard web ports 80 and 443. Both are required for Let’s Encrypt to automatically renew the certificates every 90 days anyway. This is also an opportunity to connect to the remote desktop from outside the network. I know what you’re thinking: won’t exposing port 80 stop the router’s admin screen from being accessible? No. Devices on the LAN can still see the web admin1. Devices on the WAN will be port-forwarded.

Port forwarding on the stock Netgear gateway router
Port forwarding on the stock Netgear gateway router

9. Add a DNS subdomain to Cloudflare

Let’s add a DNS entry to Cloudflare so we can connect to the remote desktop by typing a domain in the browser, not your ISP’s dynamic IP address (I’ll address this issue shortly). I’ll be using remote.armcube.com to connect to my VM running my development tools.

There is no need to bypass Cloudflare’s servers; we can take full advantage of the WAF2 and protecting our private IP. Set the IP (for now) to your ISP address. Set the TTL to automatic.

Set the Cloudflare DNS record
Set the Cloudflare DNS record

Also, click on “API” at the bottom and make note of the zone id.

Remember this zone id for the next step
Remember this zone id for the next step

For good measure, I’ve disabled cache for the remote desktop endpoint in Page Rules.

Bypass Cloudflare cache for remote desktop connections
Bypass Cloudflare cache for remote desktop connections

10. Turn Cloudflare into a Dynamic DNS provider

I use Cloudflare because I can dynamically update the DNS entry when my ISP IP changes with a script and a cron job (no need to pay noip.com or dyndns.com dynamic DNS services!).

You can write your own cron job, but the same author that gave us a self-contained Guacamole Docker image put a lot of effort into an image to update Cloudflare’s DNS entry automatically. However, with this Docker image you need to surrender your global API key and Cloudflare email address.

Never give up your global Cloudflare API key. It’s more powerful than a password because it doesn’t need 2FA to be effective. Always use scoped API tokens with minimal responsibilities, especially in third-party Docker containers.

I’ve inspected the source code and this is safe to do, but I would rather use a scoped API token. There is a fork3 that accomplishes this here. Make sure git is installed on the RPi with sudo apt install git. Get the Dockerfile and support files.

Then make the image.

You will need a scoped API token so you don’t have to surrender your global API key. Create a new API token using the DNS template.

Create a new API token with limited permissions
Create a new API token with limited permissions

Click “Continue to summary” and then “Create Token”. Copy the API token. Be sure to paste this somewhere safe because you will never see this token again. If you lose it, you will have to “roll” the token, get a new one, and delete then recreate the DDNS container.

Copy the Cloudflare API token to a safe place
Copy the Cloudflare API token to a safe place

Start the container making sure to set the API token as API_KEY, the ZONE to your top-level domain, and the SUBDOMAIN you want. With this safer fork, you must also set the ZONEID for your domain that you obtained in the previous step. You can proxy the requests as well to take advantage of Cloudflare’s security. Start the container like so.

Run the above script without the -d flag so we can see the container’s logs in the console. This way we can discover any errors or permission problems. Successful output will look like the following. Press ctrl+c to exit the container. Start it again in “detached” mode with docker container start cloudflare-ddns.

Successful Cloudflare dynamic DNS update script
Successful Cloudflare dynamic DNS update script

11. Test that your remote desktop is reachable

Try to connect to your remote desktop on your phone with 4G or LTE enabled.

Login to remote desktop from iPhone on LTE

Notice however that the connection is not yet secure.

Successful remote desktop login from iPhone

Part 2: Secure the remote web desktop connection

Many of the Guacamole guides I’ve found end here with unsecured connections. Let’s go a step further and securely connect to the remote web desktop over HTTPS (and secure web sockets) with Let’s Encrypt. Haven’t heard of Let’s Encrypt? You really should check them out if you’re manually setting up personal TLS certificates (to achieve HTTPS connections).

The plan is to use a reverse proxy called Traefik with Let’s Encrypt to achieve an end-to-end secured session.

12. Install Docker Compose for Raspberry Pi

To make life easier, let’s use Docker Compose on the Raspberry Pi using Python pip. Run the following commands4:

13. Set up Traefik for a secured end-to-end connection

Let’s use a popular reverse proxy called Traefik in conjunction with Let’s Encrypt to serve as a TLS termination point into our network. I follow the official setup guide closely.

First, create a shared network for Docker containers.

Next, create a folder to hold configuration files on the RPi and add three empty files.

Edit the traefik.toml file with the following contents making sure to use your domain and email:

Be sure to use your real email and not @example.com or else renewals will not work.

Let’s add contents to the Traefik docker-compose.yaml file next.

Let’s stop the Guacamole container. This is to ensure port 8080 is not bound any longer.

Start the Traefik container with the following Docker Compose command.

As the second to last step, let’s make a Guacamole docker-compose.yaml file and use that from now on. Edit /var/guacamole/docker-compose.yaml with this content again making sure to use your full domain including subdomain:

14. Enable WebSockets for a smoother desktop experience

We’ll want to enable WebSockets in Guacamole for better responsiveness. Since we specified a volume in the docker-compose.yaml file above, let’s edit guacamole.properties to add the line enable-websocket: true. See below.

Finally, let’s restart Guacamole to take advantage of the above reverse proxy. Remember to wait about two or three minutes for Tomcat in Guacamole to start up. Don’t visit your remote desktop URL yet, however.

When all the steps have been completed, you can verify the WebSockets are functioning by inspecting the Network panel of DevTools in Chrome or an equivalent browser. Click on the “WS” button to verify you have a similar screen to the one below.

Verify WebSockets are functioning
Verify WebSockets are functioning

15. Enable strict TLS communication in Cloudflare

You may encounter a problem with infinite redirects. Cloudflare by default uses “flexible” SSL communication (which should be called TLS). What this does is allows an HTTPS (port 443) connection to a Cloudflare proxy using their free universal SSL certificate (e.g. https://remote.armcube.com/). Then, that proxy makes an HTTP (port 80) connection to the destination (e.g. http://3.165.229.27/). Traefik will redirect those insecure HTTP requests to the HTTPS version and the loop continues forever.

To solve this, we must enable “full (strict)” SSL communication in Cloudflare. Full and strict SSL communication secures the connection end-to-end using the certificate on the Raspberry Pi (from Let’s Encrypt) from Cloudflare to our remote desktop. This causes requests to https://remote.armcube.com/ to be proxied securely to https://3.165.229.27/.

Enable full (strict) TLS communication in Cloudflare
Enable full (strict) TLS communication in Cloudflare

This is enough to enable remote desktop. However, to be even more secure, I like to enable a few more settings:

  • Enable HSTS
  • Minimum TLS Version: 1.3
  • TLS 1.3: Enabled
Make sure you set Always Use HTTPS: Off in order for Let’s Encrypt to authenticate your TLS certificates over HTTP properly.

Visit your remote desktop URL and verify the SSL Client Certificate. Here is mine.

Verify the SSL certificate is working
Verify the SSL certificate is working

16. Add basic authentication to thwart bots

This step is optional. If you want to frustrate bots, let’s add some basic authentication. This is quite easy to do with Traefik. First, create a hashed password with OpenSSL.

Generate a user:password string like .htpasswd
Generate a user:password string like .htpasswd

Next, stop guacamole.

We’ll have to slightly edit the Guacamole docker-compose.yaml file by adding the following two labels. Use the openssl output above as the string after traefik.frontend.auth.basic=.

Update docker-compose.yaml with basic auth string
Update docker-compose.yaml with basic auth string

Finally, start up Guacamole again. After a couple of minutes visit your URL and you should be presented with the basic authorization challenge.

Basic authentication successfully working
Basic authentication successfully working

17. Set up two-factor authentication with Duo

This step is optional. I like to enable MFA whenever I can, and Guacamole supports Duo for two-factor authentication. The steps are outlined in detail at https://guacamole.apache.org/doc/gug/duo-auth.html. The way I set mine up is as follows:

Edit the docker-compose.yaml next to add the following line to enable Duo:

Restart Guacamole.

After you set up your Duo login the first time you visit the Guacamole gateway, you should be presented with a similar prompt each time you log in. This should thwart keyloggers if your machine is public, suspect, or compromised.

Duo MFA screen when logging into Guacamole
Duo MFA screen when logging into Guacamole

The final results of our efforts should be:

Secure web-based remote desktop


Success: We’ve set up a remote desktop gateway server on a Raspberry Pi with Docker and Cloudflare. We can now use a web browser to securely and remotely connect to and control our target machine over HTTPS through a restrictive firewall.

Notes:

  1. Remote web management should be disabled under normal operation, but it enabled, you will have to select a port other than port 80.
  2. Web Application Firewall
  3. Source: https://github.com/drewis/docker-cloudflare-ddns
  4. Original instructions: https://withblue.ink/2019/07/13/yes-you-can-run-docker-on-raspbian.html