HTTPS for your local endpoints with inlets and Caddy

What is the problem?

Over the holidays I was reflecting on a network connectivity problem that I faced whilst employed by a large enterprise company. It turned out that this was a common problem which was being faced by my new team working at yet another large enterprise.

How do you get incoming HTTP traffic to a service running behind a restrictive firewall?

The team needed to get incoming HTTP traffic in the form of webhooks to test the work we were doing with OpenFaaS and OpenFaaS Cloud whilst developing code on their laptop with no routable IP address.

My solution for this problem is called inlets. inlets is written in Go and recently trended for about a week on Hacker News and gained over 2.2k GitHub stars along with a dozen PRs from developers in the far east. In this post we'll learn more about how to secure inlets with HTTPS for an encrypted connection.

What's the solution?

There are already several good solutions for this problem which create a tunnel from the outside world to services on our local environments - whether that be a Raspberry Pi, a home-lab or a laptop.

You can read more on GitHub about why I felt a new solution was required.

Let me introduce you to inlets.

The goal of the project is to Expose your local endpoints to the Internet.

Bill of materials:

  • an exit-node or server - this is a machine outside out firewall with full access to the Internet and a public IP address. Our users will connect to the exit-node and be routed to local endpoints inside our firewall over a websocket tunnel
  • a client - the client acts as a reverse proxy or bridge - when it hears a request, it will proxy that to a local service such as an Express.js server and then send a result back
  • A permanent tunnel using a websocket - most corporate firewalls will allow an outbound TCP connection to be established over your existing HTTP/S proxy using a CONNECT message.

Each HTTP request to the exit-node is serialized and published on the websocket as a control message - then blocks. A client will then receive the request, decide if it knows how to proxy that site then will fetch the resource and send it back down the websocket as a serialized response.

Finally the user's HTTP request unblocks and writes the response to the caller.

Securing the tunnel

By default, for development inlets is configured to use a non-encrypted tunnel which is vulnerable to man-in-the-middle (or MITM) attacks. I have a roadmap item to bake TLS into inlets using the new libraries made available by Caddy, but for the time-being we can do this by running the Caddy binary on our exit node.

Enabling HTTPS means that our users connect to an encrypted endpoint and our inlets clients can also connect to our server over an encrypted tunnel.

Tutorial

For our exit-node we will use a DigitalOcean droplet, but if you are really on a budget you can use a cheaper VPS like Scaleway.com. The main requirement is that we have a VM or VPS with a public IP address.

This is the conceptual diagram of what we're going to create:

IMG_20190315_120300

Setup an exit node

  • First sign-up to DigitalOcean using my code to get free credits: 100 USD credit for 60 days.

  • Create a Droplet in a region near you using Ubuntu 18.04.x

  • The cheapest Droplet is suitable for our purposes - 5 USD with 1GB RAM / 1vCPU and 1TB transfer

  • Select additional options. Here you can pick User data to automate the setup of the inlets server

  • Enter your user-data using the text from our userdata.sh script.

  • Add any SSH keys you want to use for logging into the exit-node

  • Name your host i.e. inlets-exit-node-1

  • Deploy the droplet

  • Get your public IP address

Configure DNS

  • Now create an A record for your domain name pointing at the new Public IP address of your exit node. I used Namecheap.com to provision a cheap domain name for about 2 USD. You may have even just bought a .dev domain in all that craziness. This is a good opportunity for you to put it to use.

Enable Caddy for HTTPS

  • Log in and update the port from 80 to 8080 with:
sudo sed -i s/80/8080/g /etc/systemd/system/inlets.service
sudo systemctl daemon-reload 
sudo systemctl restart inlets

We are doing this so that Caddy can run on both port 80 and 443.

  • Now grab your token for authenticating your client:
sudo cat /etc/default/inlets | cut -d"=" -f2

cd4bf5db2601ec9075425102d2b12a9ee5413d4a
  • Download the latest Caddy binary from the Releases page - on a VPS you want a binary with a name like caddy_v0.11.5_linux_amd64.tar.gz. You can use wget https:// to download the file.

  • Uncompress the tar.gz file: tar -xvf caddy_v0.11.5_linux_amd64.tar.gz

  • Create a Caddyfile replacing exit.domain.com with your own DNS record:

exit.domain.com

proxy / 127.0.0.1:8080 {
  transparent
}

proxy /tunnel 127.0.0.1:8080 {
  transparent
  websocket
}
  • Run Caddy in a new terminal run tmux and start the Caddy process with ./caddy. The first time you run this command, Caddy will ask for your email address. Enter your email and then wait for the TLS certificate to be issued by LetsEncrypt.

Connect your client

Over on your laptop you can now start your local endpoint either directly or via a Docker container.

The simplest HTTP server is probably the one built-into Python. It will serve files from whatever directory you run it in.

  • Create a temporary directory:
mkdir ~/filestore/
cd ~/filestore/
echo "Welcome to my filestore" > welcome.txt
# If Python version returned above is 3.X
python3 -m http.server
# On windows try "python" instead of "python3"
# If Python version returned above is 2.X
python -m SimpleHTTPServer

By default it listens on port 8000: Serving HTTP on 0.0.0.0 port 8000 ... so this will be our --upstream value.

You can test the local URL at: http://127.0.0.1:8000

Connect your client

Install inlets on your laptop or local computer such as your Raspberry Pi:

# Install to /usr/local/bin/ (recommended)
curl -sLS https://get.inlets.dev | sudo sh

# Install to local directory
curl -sLS https://get.inlets.dev | sh

Now open the tunnel:

inlets client \
 --remote wss://exit.domain.com \
 --upstream=exit.domain.com=http://127.0.0.1:8000 \
 --token=cd4bf5db2601ec9075425102d2b12a9ee5413d4a
  • Note the use of --token from earlier. This authenticates our client to our exit-node to prevent unauthorized access.
  • wss:// shows we are using an encrypted tunnel to prevent tampering and MITM

Now you and your friends can visit https://exit.domain.com and access the Simple Python HTTP server running on your laptop.

If you have multiple domain names and multiple services on your laptop simply change the --upstream flag to reflect that.

For OpenFaaS on Docker Swarm that may be:

inlets client \
 --remote wss://exit.domain.com \
 --upstream=gateway.domain.com=http://127.0.0.1:8080,prometheus.domain.com=http://127.0.0.1:9090

You'll see output similar to:

2019/03/15 11:56:31 Upstream: lon1-exit.domain.com => http://127.0.0.1:8000
2019/03/15 11:56:31 connecting to wss://lon1-exit.domain.com/tunnel
2019/03/15 11:56:32 Connected to websocket: 192.168.0.71:49631

Just remember to add each DNS A record for each sub-domain you want to be accessible from the exit-node.

Did you know?

inlets also works on a Raspberry Pi, so you can run the client on a Raspberry Pi 24/7 as a way to get incoming traffic to your services on your Raspberry Pi cluster, or as a cheap gateway pointing at other computers in your network.

Link: Buy your Raspberry Pi cluster here and follow my latest tutorial: k3s: Will it cluster?

Wrapping up

You now have a completely free tunnel set up for around 5USD/month which can punch through almost any firewall. The code is open-source under the MIT license and built by community.

You can view the raodmap on GitHub. Contributions are welcome and for comments, questions or suggestions you can follow me on Twitter @alexellisuk

Show your support: Star or fork the project here: alexellis/inlets

Do you enjoy my work? Buy me a coffee through Patreon.

Alex Ellis

Read more posts by this author.

Subscribe to alex ellis' blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!