Have you ever been on and about and wanting to connect to your VM, only to realize you don't have the key on your device? Perhaps you then thought to go back to password-based authentication. Have you forgotten your password? Well, nowadays you should be using "must-have" tools such as Bitwarden for that anyway.
Or, you could forget all the above nonsense and put Cloudflare in front of your SSH service. This could give you a browser-based "Zero Trust" set up in a matter of minutes - thanks to the automation capabilities via Terraform.
I've been meaning to try this setup for quite some time now, and in this article, we will look at it together. The code is also available on Github, feel free to clone the repo and have a look at it while you read this.
Terraform installed on your machine (I'm using Terraform v1.0.7).
Of course, you can experiment and use other IaaS / PaaS providers or even your own VM, or swap in Github for another supported Identity Provider. In that case, you will need to tweak my Terraform configuration accordingly.
In terms of costs, the only part that is not free is the Digitalocean Droplet. However, the cost should be quite minimal since we will use the smallest VM size possible s-1vcpu-1gb (unless you change it in the configs). You can destroy your test environment easily with Terraform once you have played with it.
The Cloudflare for Teams setup described is achievable with the Free Plan, which supports a maximum of 50 users (we use one in my demo).
Architecture & Requirements
In the tutorial, we will have a Virtual Machine running sshd in Digitalocean.
Our objective is to allow external users to reach our VM via SSH from anywhere (office, home, beach bar wi-fi etc...).
The access must be authenticated using a standardised, auditable Single Sign-On mechanism.
Additionally, the SSH endpoint of our VM must be protected and present as little attack surface as possible from the exterior.
Let's have a look at the key parts of the Terraform configuration from the repository I shared.
In the digitalocean.tffile, we have our usual - if you have read my previous tutorials - structure for deploying our VM on Digitalocean.
Most notably, we pass a Cloud-Init template to the VM, including configuration variables. Cloud-Init will then take care of all the bootstrapping tasks based on our instructions.
To use it, we need to install a lightweight daemon (cloudflared) on the VM. This will create outbound encrypted connections to Cloudflare's edge datacenters near the VM. This allows us to route all incoming traffic to the SSH service to Cloudflare;s Edge, where we can perform the authentication checks before forwarding it to the VM.
At the same time, the VM itself can be locked down and reject all incoming traffic, since we will rely on the tunnel connection that was already created from within. Pretty clever!
For the tutorial, we will still configure the VM to accept incoming TCP connections on port 22 (SSH), but only from the IP of the machine we are using to run the demo. This is because Terraform needs to use SSH to manage the resource, and also because it's handy to have when experimenting.
Above, the Tunnel configuration includes a secret value that will be used by cloudflared to authenticate with the Cloudflare Edge servers and establish the tunnel. This resource is then passed to the Cloud-Init template, which will generate the necessary configuration files on the VM to run cloudflared successfully.
We then have our Access Application, protecting the FQDN that end-users will reach to start their SSH session.
The Access Policy determines who can use our application and authenticate. In the tutorial, we include a single user by e-mail address, and we require this user to be authenticating using Github.
The integration with Github OAuth is at the bottom. It requires two configuration parameters: client_id and client_secret. To obtain these, you will need to navigate to your Developer Settings > OAuth apps in your Github account. Create a new OAuth App and include the following values.
🙋 If you cannot remember your Cloudflare Teams Organization name, head to "Settings" > "General" to find it.
Once done, you will see your client_id and you will be able to generate a client_secret as well. Copy the values somewhere safe and paste them in your terraform.tfvars file, alongside all the other parameters.
This is an initialization file used by the VM to start up and create the required environment for our tutorial.
With these, we use the template variables passed by Terraform and tell Cloud-Init to create our two Cloudflare Tunnel configuration files. The first one /etc/cloudflared/cert.json contains the credentials for establishing the connection from the VM to Cloudflare's Edge.
The second file contains the actual Tunnel configuration including the ingress rules which send the Tunnel traffic to the correct endpoint based on expression filters. In our case, we send all the traffic matching the public FQDN of our SSH service to the local SSH port, and for anything else, we will just serve an HTTP 404.
The adduser line will come in handy in the latter part of the tutorial, where we will set up short-lived certificate-based authentication. For that, we will need a user on our VM which will match with the SSO identity. We will pull this identity from the e-mail address of the authenticated user.
The rest of the commands are used to download and install the cloudflared daemon, and then run it as a service. This enables it to restart after reboots and establish a connection to Cloudflare.
This file is a template for your terraform.tfvars configuration file. Most importantly you will need:
For the Cloudflare API token, I've set the following permissions for it:
Access: Service Tokens:Edit
Access: Organizations, Identity Providers, and Groups:Edit
Access: Apps and Policies:Edit
Access: Apps and Policies:Edit
It's time to see some action!
After you created and populated your terraform.tfvars file, you are ready to start.
We use the terraform init command to download the Cloudflare and Digitalocean providers needed for running all the set-up calls.
Next, terraform plan -out plan.txt will give you an overview of the resources and planned changes. This is your chance to review that you are not about to provision a nuclear powered, super-expensive top of the line Digitalocean droplet by mistake 😅.
Ready? Then it's time for terraform apply "plan.txt" - if everything goes smoothly you should be welcomed with a message including a few useful commands
We could ssh from our terminal using the command shown: this would connect directly to the Digitalocean VM on port 22, bypassing Cloudflare.
Instead, let's try to reach our Access application in our browser. Here is my test:
Quite cool isn't it? We are prompted to authenticate via our selected Single Sign-On provider (Github). Then we are redirected in the browser and we can enter the user ( root in our example) and the private key/passphrase and let Cloudflare handle the rest. Eventually, we are connected to our droplet directly in our browser.
Still, this is not as smooth as it could be. We need to specify the ssh user, and then we need to pass our key (very much in the same way as we would do from our terminal). We can improve this even further by enabling authentication via Short-Lived Certificates:
Cloudflare Access can replace traditional SSH key models with short-lived certificates issued to your users based on the token generated by their Access login. In traditional models, users generate a keypair and commit their public key into an infrastructure management tool, like Salt, or otherwise upload it to an administrator. These keys can remain unchanged for months or years.
Cloudflare Access removes the burden on the end-user of generating a key, while also improving the security of access to infrastructure with ephemeral certificates.
Let's do this!
Note: the below steps can in fact be automated with Terraform. The latest version of my code in Github will do these steps for you automatically. Just download the latest version instead of the 1.0 tag to see the configuration.
There are many reasons (as seen above) why it is a good idea to move away from SSH public keys and use instead short-lived credentials that are generated as needed when the authentication happens. There is an extensive blog post that dives into the reasons and also explains fully how the Cloudflare approach work.
Since the explanation is already covered in the above link, we will focus instead on trying it out ourselves. To set this up, we will need to head to our Cloudflare for Teams dashboard and then generate the Certificate for our application and retrieve the associated Public Key:
Once generated, copy the public key and head back to your VM. We will update the SSH Daemon configuration to use this form of authentication so that we can streamline our browser-based access even further. You can head back to your in-browser, root SSH session to continue the setup from there.
First, we save the public key from above in /etc/ssh/ca.pub , then we modify the SSH Daemon configuration to enable Public Key authentication and link the above key as trusted.
I have recorded the steps in the below video.
And now - we can close our browser and try again ...
Very smooth! We get seamlessly authenticated into our SSH session and we are recognized from the information received from the identity provider. Of course, we can access our VM from anywhere now - and provided that we do not set up more restrictive policies for our application with Cloudflare Access.
In a multi user scenario, it could be very handy to add and revoke access permissions to various team members as needed, or even temporary collaborators. All the login attempts are also audited and can be reviewed in the Cloudflare for Teams dashboard 😎.
Once you're happy with your tests, do remember to issue a terraform destroy to remove all the resources we created
I hope you enjoyed this tutorial. Let me know if you have any questions in the comments section below!