How to set up Argo Tunnels for Remote Access to Local Development Sites

In this blog post, I want to share my experience with Cloudflare’s Argo Tunnels, and how they helped me solve a common web development problem. Why use Cloudflare for this, you ask? Because of the wild array of options, optimizations and performance enhancements it brings. We’re huge fans of Cloudflare here at Servebolt and can’t recommend them enough. Especially starting with the Cloudflare Pro plan which adds extra security to boot.

As developers, we often work on something on our laptops and want to have it accessible from another machine. Sometimes we want to show our work to a client or coworker, use it in a demo, or for a workshop. Other times we are developing an integration with an external service that needs to call webhooks on our site. The fundamental problem with any of these is that our developer laptops do not have a public IP or DNS name to address from a browser.

There are other solutions I’ve considered. So far, I’ve been pushing my code to a staging site with a public IP. This required an entire second machine set up with the same environment (in Servebolt parlance, another site on my Bolt). Having to copy my work to this site before checking my work introduces long turnaround times for changes. And that’s not to mention the complicated debugging situation.

I recently had to implement support for external webhooks. I wanted to be able to make edits to my code from my Macbook, and immediately see the result of the webhook. This required that I had a public IP and DNS name for the external service to call my webhook on.

What are Argo Tunnels?

Cloudflare’s lightweight Argo Tunnel daemon creates an encrypted tunnel between your origin web server and Cloudflare’s nearest data center. Argo Tunnels are not to be confused with Argo Smart Routing. There’s no need to open any ports in your firewall, and all traffic gets encrypted. The tunnel is easy to set up for anyone with little to no administration experience. All while minimizing the risk of a dangerous misconfiguration.

Argo Tunnels are typically used to shield an origin server from DDoS attacks and data breaches. This is due to the tunnel not exposing the origin server directly. In my situation, I’m simply setting up a tunnel to use my Macbook as the origin server for a local website that I am building. Not because I want the extra security, but because that creates a public IP address from which traffic is tunneled to my Macbook.

Installing the cloudflared software and setting up Apache

I followed the installation guide on Cloudflare’s site to install the daemon via Homebrew. Then I linked the tunnel to my domain on Cloudflare.

$ brew install cloudflare/cloudflare/cloudflared
$ cloudflared tunnel login

I have access to a domain named enno.horse that’s managed by Cloudflare, which I then select from the web page that opens in my browser, and authorize to be used by the tunnel. This downloads a certificate file to my machine, which the cloudflare daemon will use to authenticate and establish the tunnel.

On my Macbook, Apache serves the site I’m working on at http://mysite.local. I can now set up a tunnel from that site to a subdomain like so:

$ cloudflared tunnel --hostname ennos-mbp.enno.horse --url
http://mysite.local

Note that I don’t have TLS set up, but because all traffic between me and cloudflare goes through an encrypted tunnel, and I will use Cloudflare’s HTTPS termination for ennos-mbp.enno.horse, all traffic between the user’s browser and Cloudflare is also encrypted, leaving any sensitive data protected.

This doesn’t quite work yet, and it’s serving me my Apache default page, and not the site. That’s because my Apache virtualhost configuration has a ServerName entry for mysite.local, an /etc/hosts alias for localhost, but does not know about ennos-mbp.enno.horse.

I add a ServerAlias ennos-mbp.enno.horse line to the virtualhost configuration in /usr/local/etc/httpd/extra/httpd-vhosts.conf, and once I reload Apache with apachectl graceful, I can see my site’s front page from anywhere in the world!

Running as a system service

While this is nice, I have to start the tunnel in a terminal every time I want my site to be reachable. It also stops working when I restart my Mac. What I really want is for the tunnel to run as a system service controlled by macOS’s launchd. This means it’s started automatically and can be controlled like any other system service. To do this, I first define my tunnel by creating ~/.cloudflared/config.yml:

hostname: ennos-mbp.enno.horse
url: http://mysite.local:80

The cloudflared binary knows how to install itself as a user service, and will do so when I run:

$ cloudflared service install

Now the tunnel gets established automatically, and if I want to disable external access to my site at any time, I can stop and start the service using these launchctl commands:

$ launchctl stop com.cloudflare.cloudflared
$ launchctl start com.cloudflare.cloudflared

Extra Credits: Drupal problem, solved

The site I’m working on is a legacy Drupal 7 site, and I noticed it was rewriting all URLs to link to http://mysite.local, which would manifest as a mess of mixed-content warnings and hyperlinks that took me away from my https://ennos-mbp.enno.horse domain, which obviously didn’t work on any other machine than mine.

The problem and solution here is related to Drupal 7’s global $base_url variable. It’s set to my local URL, and so every link that’s created by Drupal will use it. I can’t change it, either, because that will break my local development site, unless I set up a multi-site environment. That’s no easy feat, so I came up with the following little hack:

$proto = 'http';
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
    $proto = $_SERVER['HTTP_X_FORWARDED_PROTO'];
} elseif (!empty($_SERVER['HTTPS'])) {
    $proto = 'https';
}
$base_url = $proto .'://' . $_SERVER['SERVER_NAME'];

Since Cloudflare sets the X-Forwarded-Proto header, I can tell whether a request came in through a https URL, and together with the SERVER_NAME variable, I can set the correct base_url for each request during Drupal’s bootstrapping process by adding this little snippet to settings.php. It may not be pretty, but it does the job.

Alternatives considered

I could have also used an SSH tunnel to a port on an external machine. That’s more difficult to get right, since it involves firewall changes and a lot more moving parts. It also wouldn’t be easy to make it run as a system service the way cloudflared does.

A similar all-in one solution that I initially considered is ngrok, but I eventually settled on Cloudflare’s Argo Tunnels. This decision was made easier since I already had a domain that was managed by Cloudflare.

In the end, the combination of the easy setup and other facilities, like easy HTTPS termination, made Argo Tunnels look like the best solution.