Internal services on public domain

In the post before I told you a bit about my experience with Home Assistant. Now I want to make my Home Assistant available to the outside world via a (sub)domain, so that I can connect the HA app on my phone for example. There were two problems to solve:

  1. The modem of my internet provider seems to block all incoming connections. It does not allow to specify a host/ip as DMZ. Initially I thought that this is pretty annoying. On the other hand it probably adds a nice security layer. Solution: I need to setup an ssh tunnel from the HA machine to an outside machine with a public IP.

  2. I don’t want to register a domain and/or IP for each service I want to expose (might also do the same for NextCloud later, etc.). So on the machine with the public IP I have to setup subdomains and proxies which redirect from the subdomain https://ha.mydomain.com to the port of the tunnel http://localhost:1234 . There’s lots of docs on this for Nginx, but not so much for Apache, so this was pretty fiddly, but I got it working in the end.

1 - The tunnel

#!/bin/bash
user="john"
public_ip="1.2.3.4"
public_port="1234"
service_ip="192.168.1.123"
service_port="4321"

while :
do
	date
	echo "open ssh connection"
	ssh -o ExitOnForwardFailure=yes -o ServerAliveInterval=60 -o ServerAliveCountMax=5 -R 0.0.0.0:$public_port:$service_ip:$service_port -N $user@$public_ip
	echo "ssh connection closed... waiting 10min until restart..."
	sleep 600
done

This is a little bash script that maintains an SSH tunnel between a machine which runs a service (service_ip) on a certain port (service_port) in the LAN and a machine with a public ip address (public_ip). Some SSH options explained: ExitOnForwardFailure makes sure the ssh command fails if the port is already in use (or other ‘secondary’ issues). ServerAliveInterval and ServerAliveCountMax ensure that the ssh server doesn’t close the connection due to inactivity (client will send null packets in the given interval). -R establish remote port forwarding. -N specifies that you only want a tunnel, not a remote shell. If for whatever reason the SSH connection is terminated, the script will wait 10min and then try again. Note: This script should obviously run on a machine within the same LAN network as the ‘service’ machine; and you have to setup public key authentication for $user on the $public_ip machine (e.g. using ssh-copy-id).

2 - The proxy

On the Apache server add a named virtual host for the subdomain, which only purpose is to redirect to https:

<VirtualHost *:80>
    ServerName ha.mydomain.com
    RewriteEngine on
    RewriteCond %{SERVER_NAME} =ha.mydomain.com
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Then add the subdomain to the ssl config with the specific redirect to the local port (which you’ve tunnel with previous script):

<IfModule mod_ssl.c>
    <VirtualHost *:443>
        ServerName ha.mydomain.com
        RewriteEngine on

        ProxyPass "/" "http://localhost:1234/" upgrade=websocket
        ProxyPassReverse "/" "http://localhost:1234/"

        Include /etc/letsencrypt/options-ssl-apache.conf
        SSLCertificateFile /etc/letsencrypt/live/mydomain.com-0001/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/mydomain.com-0001/privkey.pem
    </VirtualHost>
</IfModule>

I used cerbot/letsencrypt to set SSL for the server. It might look a bit different for you. Note: You might have to run certbot --apache --expand in order to get a new certificate which includes the subdomain!

See the upgrade=websocket option. This is very important and took me ages to find out! You have to enable websocket proxy for HA, and that’s the line you need.

If you use Nginx instead of Apache, just google, there are plenty of examples.