How to securely expose your local app to the internet using EC2?

Ali Haydar
5 min readJan 31, 2023

Are you developing an app and want others to access it before it’s available in the cloud? Or are you integrating with a third-party tool and wish to enable access to your local app for a better development experience? This article is for you.

There are many use cases where you might need to expose your local app (the app you are developing on your local machine) to the internet. That could be a site, an API or a chatbot. I’ve worked on a few of these cases, most recently a side project developing a Slack application.

When integrating with Slack, you need to provide Slack with a redirection URL so that the user goes back to your site after granting the necessary permissions (this is common when using [OAuth](https://en.wikipedia.org/wiki/OAuth)).

As you are working on a local machine, the URL should point to your localhost, which has a private IP. Your home/office router will have a public IP assigned by the internet provider, but any device behind that router will be given a private IP. So how can we expose our app to the internet?

How to expose your app to the internet?

Note that this setup might incur some costs.

First, I used ngrok, which creates a tunnel between my local machine and servers that are already exposed to the internet — So your requests to the ngrok server get forwarded to your local app. That’s convenient and useful. However, the URL of that server changes every time you connect, which means I need to update my app config every time I connect.

I wanted something more permanent and maintainable, where I start my app, add my changes and test. One option was to subscribe to the ngrok paid services, where you can get a permanent URL. I need this for a side project and want it to be cost-effective, so I decided to implement a basic solution myself.

To achieve this, we’ll create an EC2 instance that allows ingress access from the internet, and install an nginx server that acts as a reverse proxy to forward requests to this server to my app running on my local machine, using SSH tunnelling.

Let’s build it with Terraform.

## Create an EC2 instance

We will use the t2.micro instance as it’s covered in the Free Tier for 12 months.

First, we will create a Security Group, which will be attached to the EC2 instance, to allow ingress HTTP and ssh access to the machine:

resource "aws_security_group" "allow_http_ssh_and_http_on_80" {
name = "allow_http"
description = "Allow http inbound traffic"
ingress {
description = "http"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "ssh"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "allow_http_ssh"
}
}

We need a key pair to SSH in the EC2 instance, so I used one that I had already created using the Terraform data source:

data "aws_key_pair" "ec2_instance_key_pair" {
key_name = "ec2-instances"
}

You could create a new key pair using the aws_key_pair resource.

Next, we will create an EC2 instance — I’ll install and set up nginx on this instance as user data. That’s the script that runs after the instance starts.

resource "aws_instance" "port_forwarding_server" {
ami = "ami-06bb074d1e196d0d4"
instance_type = "t2.micro"
key_name = data.aws_key_pair.ec2_instance_key_pair.key_name
vpc_security_group_ids = [aws_security_group.allow_http_ssh_and_http_on_80.id]
user_data = <<EOF
#!/bin/bash
echo "installing nginx"
sudo amazon-linux-extras install nginx1 -y
echo "updating nginx config for reverse proxy"
echo "
user nginx;
worker_processes auto;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
error_log /dev/null;
access_log /dev/null;
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream express_server {
server 127.0.0.1:8080;
keepalive 64;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Host \$http_host;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://express_server/;
proxy_redirect off;
proxy_read_timeout 240s;
}
}
}" | sudo tee /etc/nginx/nginx.conf

## Starting Nginx Services
sudo chkconfig nginx on
sudo service nginx start
sudo service nginx restart
EOF

tags = {
Name = "local-dev-tunneling-server"
}
}

This server will receive requests on port 80 and forwards them to the localhost server on port 8080.

The only thing that’s left to do is to start your local app and start a remote ssh port forwarding session by running the following command:

ssh -i ~/.ssh/ec2-instances.pem -R 8080:localhost:8080 ec2-user@<public-ipv4-dns>

You can copy the instance Public IPv4 DNS from the AWS console.

Now we need to have the site secure with SSL/TLS. So we can either add a load balancer and associate it with a certificate from AWS ACM or directly create a certificate on the instance. Let’s do the latter using OpenSSL.

  • First, enable ingress access of HTTPS on port 443 on the security group
ingress {
description = "https"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [
"0.0.0.0/0"
]
}
  • Update the startup script to create the certificate and use it in the nginx config. Update the user_data tag in Terraform to:
user_data = <<EOF
#!/bin/bash
echo "installing nginx"
sudo amazon-linux-extras install nginx1 -y
echo "create a cert"
sudo mkdir /etc/ssl/private
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt
echo "updating nginx config for reverse proxy"
echo "
user nginx;
worker_processes auto;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
error_log /dev/null;
access_log /dev/null;
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream express_server {
server 127.0.0.1:8080;
keepalive 64;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl;
ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
server_name _;
location / {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Host \$http_host;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://express_server/;
proxy_redirect off;
proxy_read_timeout 240s;
}
}
}" | sudo tee /etc/nginx/nginx.conf

## Starting Nginx Services
sudo chkconfig nginx on
sudo service nginx start
sudo service nginx restart
EOF

Now navigate to the Public IPv4 DNS provided by EC2 in your browser, which should direct the user to your local app on HTTPS.

How could you further improve the experience? I’d like to hear your thoughts.

Thanks for reading this far. Did you like this article, and do you think others might find it useful? Feel free to share it on Twitter or LinkedIn.

--

--

Ali Haydar

Software engineer (JS | REACT | Node | AWS | Test Automation)