Reverse Proxy with SSL
Published: 2022-08-14 21:08:00

## Introduction

It's often useful to make an application under local development available temporarily on the public internet. At [Blended Edge](https://www.blendededge.com/), for example, our work with automating integrations means we run a lot of OAuth2 flows, and using OAuth2 for an application under development (usually) requires a publicly available endpoint with an SSL certificate.

[Ngrok](https://ngrok.com/) is a reverse proxy tool that makes this very simple, but it's also fairly easy to set up a reverse proxy using an AWS EC2 instance, certbot from Let's Encrypt, and the `ssh` command.

This walk-through assumes some basic knowledge of DNS, `ssh`, and EC2 instances (or similar cloud compute resources).

## Create a Remote Server

We're going to start by launching an EC2 instance on AWS to act as our remote server. This could of course be done with any cloud provider, such as Azure or Digital Ocean.

Set the instance name as "Reverse-Proxy", and under "Application and OS Images" select Ubuntu. At the time of writing this the default Ubuntu image is 22.04.

![Create a new Ubuntu instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g71ihqglyuayfd4gfzhr.png)

I have an existing SSH key pair that I will use for accessing this instance, but if you don't have one or would like to use a new key pair simply click "Create new key pair".

Create a new security group that allows HTTP/HTTPS traffic from any IP address, but limit the SSH traffic to just your own IP.


![Set up the security group](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hntbrsln6v4mgyzpzob.png)

Click "Launch Instance", navigate to the instances overview page, and wait for the status checks on the new instance to pass. This may take a couple of minutes.

Once the instance is ready, go to the overview page for the instance and click "Connect" in the upper right hand corner. Follow the steps to connect.

```sh
# if this is a new ssh key you may need to specify the path to the key
# ssh -i /path/to/key.pem <user>@<address>
ssh ubuntu@ec2-13-59-152-133.us-east-2.compute.amazonaws.com
```

## Install Nginx

Update the package list, upgrade the packages, and install nginx.
```sh
sudo apt update
sudo apt upgrade -y
sudo apt install nginx
```

If you send a GET request to your instance's IP address via a web browser or curl you should get the default nginx page.

```
curl http://13.59.152.133
```
```html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...
```

## Domain and SSL

Point a domain to the public IP address of your instance. The nameservers for my domain `hydewd.com` are with DigitalOcean, so in my case updating the `A` record to point the domain at my instance looks like this:

![DNS A Record](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8isbg6158d89raghlrmj.png)

Note: If you are doing any kind of serious or long term work with this setup you should allocate an elastic IP address, associate it with your instance, and point your domain to the elastic IP.

Install `certbot` from Let's Encrypt to handle automatically setting up the SSL certificate.

```sh
sudo apt install certbot python3-certbot-nginx -y
```

Now run have `certbot` make the request for an SSL certificate. Here I am specifying that I want to cover both the naked domain `hydewd.com` as well as `www.hydewd.com`. You will be prompted to provide an email address for renewal notices, asked to agree to the terms of service, and asked if you are willing to share your email with the Electronic Frontier Foundation (this last one is optional).

```sh
sudo certbot --nginx -d hydewd.com -d www.hydewd.com
```

If everything has worked you should now be able to load the default nginx page at `https://<your-domain>`.

## Nginx Configuration

The next step is to tell nginx to redirect http/https requests to another port (`8080`). In the following step we will tunnel that port to our local machine.

```sh
sudo vi /etc/nginx/sites-available/default
```

Within the `location` block of the default `server` block, add a line to forward traffic to `http://127.0.0.1:8080`.

We will be changing the contents of the `location` block. Here is what it looks like to begin with:
```
    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }
```

Change the contents of the `location` block to:
```
    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://127.0.0.1:8080/;
    }
```

Save the file, and ensure that the configuration is valid with the command `sudo nginx -t`. If there is no error, go ahead and restart nginx to apply the latest changes.

```sh
sudo nginx -s reload
```

## SSH Remote Forwarding

The final step is to tunnel the requests from the server's port `8080` to the local machine. This can be done with the `ssh` remote forwarding command.

```sh
ssh -R 8080:localhost:8080 ubuntu@ec2-18-217-182-155.us-east-2.compute.amazonaws.com
```

Now run a server locally on port `8080`. Here, for example, is a simple node server using the express framework that will reply to all `GET` requests with some information about what the server sees in the incoming request.

```sh
mkdir local-server && cd local-server
npm init -y
npm install express
```

Create a file called `index.js` with the following contents:
```js
const express = require('express')
const app = express()
const port = 8080;

app.get('*', (req, res) => {
  const { baseUrl, headers, hostname, ip, method, originalUrl, path, protocol } = req;
  res.json({
    baseUrl,
    headers,
    hostname,
    ip,
    method,
    originalUrl,
    path,
    protocol
  })
});

app.listen(port, () => console.log(`App listening at http://localhost:${port}`));
```

```sh
node index.js
```

Sending a query to your domain will now return a response from your local express server.

```sh
curl -s 'https://www.hydewd.com/some/path?some=query&another=123'
```

Example response:
```
{
  "baseUrl": "",
  "headers": {
    "host": "www.hydewd.com",
    "x-real-ip": "104.1.2.3",
    "connection": "close",
    "user-agent": "curl/7.68.0",
    "accept": "*/*"
  },
  "hostname": "www.hydewd.com",
  "ip": "::ffff:127.0.0.1",
  "method": "GET",
  "originalUrl": "/some/path?some=query&another=123",
  "path": "/some/path",
  "protocol": "http"
}
```

Note that the header `x-real-ip` displays the IP address of the end user making the request, not the IP of the instance. This is a result of the configuration line `proxy_set_header X-Real-IP $remote_addr;`.

## Conclusion

This approach will work for some basic use cases, but is overall quite limiting. In a future post we will look at using Traefik to provide a more feature rich setup.

## Helpful Links

Digital Ocean Guides: [How to Install Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04)

Nginx Docs: [Reverse Proxy Documentation](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)

Nginx Docs: [Installing Lets Encrypt](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)