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:
-
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.
-
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.