This is a third post in the Smart Home series. See the previous post if you haven’t already: https://blog.michal.pawlik.dev/posts/smarthome/zigbee-setup/. Last time I wrote about Zigbee and shown how to control smart home devices. In this post I want to show you one of many ways you can use to expose your HA (Home Assistant) instance to the internet.

Why expose?

All of the examples in previous posts were referring to local instance of Home Assistant. To work with it you had to be with the same network. While it’s not a problem when you are at home, there are cases when you want to access HA from the external network. Those cases include controlling the heat, accessing monitoring, switching light etc. when you are not home.

How to externalize

DMZ

The first and quite common approach is to externalize the instance through your router. It is only possible if your ISP provides you with a public IP address, which means your home router is reachable from the internet. Some ISPs charge extra for that, some don’t offer it at all for individual clients, which renders this approach not suitable for some users.

Externalization using this method also brings some risks. Depending on implementation users either forward specific ports or set up a DMZ to the machine where HA is hosted.

This is a one way to go, but due to it’s limitations and potentially large surface of vulnerabilities introduced. It’s also well described already so there’s no point in me repeating the knowledge.

Instead I want to share a different approach, based on reverse ssh tunnel and an reverse proxy.

Reverse Proxy

Overview

Reverse proxy approach doesn’t require the public IP, instead it uses a VPS. If you have decided to self host there’s a good chance you already have one. Otherwise you can either buy the cheapest one you can find or ask a friend who has one to help you with that.

Before digging down to the implementation, let’s see the solution architecture:

architecture overview

The whole thing works because VPS is externally available, and once it receives traffic for a specific domain, it’s forwarded to the HA instance. VPS can access the HA instance thanks to the tunnel, initialized from the private HA network, but thanks to it’s reverse nature it allows the traffic to go back there from VPS.

Implementation

There are two components to be implemented: reverse proxy and ssh tunnel. Let’s start with proxy.

Reverse Proxy

The reverse proxy can be set up in many ways. My preference here is Caddy server. To forward the traffic to reverse tunnel, you can use a following snippet:

homeassistant.mydomain.com {
  reverse_proxy http://127.0.0.1:9090
}

Quite simple ain’t it? This snippet assumes your reverse tunnel is exposed on port 9090 and that your subdomain is homeassistant.mydomain.com.

If you wish to introduce an additional layer of security, you can add Basic Auth, so before accessing the instance you’ll need to provide extra pair of login and password. This makes brute force attacks against your instance much more difficult.

To implement that, you need to generate password, for that you can use caddy hash-password and use the generated password like this:

ha.michalp.net {
  reverse_proxy http://127.0.0.1:9090
  basicauth * {
    MyBasicAuthUserName JDJhJDE0JGc0YUYzZGVSZklkYjBHOFM2MGtCSy45Rm9vUkYydkFwbjRZU0tVejl6VU1OVlJ6cXhNZXBx
  }
}

Where MyBasicAuthUserName is your username and JDJhJDE0JGc0YUYzZGVSZklkYjBHOFM2MGtCSy45Rm9vUkYydkFwbjRZU0tVejl6VU1OVlJ6cXhNZXBx is the generated password hash.

Reverse SSH Tunnel

Having the reverse proxy ready, let’s set up the tunnel. Such tunnel needs to start with your home assistant instance, keep the connection alive and so on. Luckily enough, you don’t need to write any code, since I’ve published this part as a Home Assistant addon.

Here’s how to use it:

  • In your user settings enable Advanced Mode
  • Go to Settings > Add-ons > Add-on Store (bottom right)
  • Open the three-dot menu on the top right
  • Add following repository: https://github.com/majk-p/home-assistant-addons
  • Refresh the page so the new repository shows up
  • Install the Reverse SSH Tunnel addon
  • In the configuration provide the necessary details, you can use the yaml view to set it up like this:
username: tunnel-user-name
private_key: |
  -----BEGIN OPENSSH PRIVATE KEY-----
  b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
  NhAAAAAwEAAQAAAYEA7nwnrZzm2PM6OQB0juB3PDd7Z0wsJsTa11gRPtzeYkRGXdmHRTXm
  ......................................................................
  8pBXR0LFQcVTRC6iUtLjabH3GjHh6w/EpX3OoVqlwY5dNWlWJB0yj5dc3evxbRJZWw5c9O
  KykTD+AymlAZ1QZoNysME8TjrGBeUiO4yOdlg9ejLM7B090IUzdaJfgcoFIRyvNO54ue+K
  2N6RDpQMW0rr9rAAAADm1hamtwQGluc3Bpcm9uAQIDBA==
  -----END OPENSSH PRIVATE KEY-----  
server:
  host: homeassistant.mydomain.com
  port: 9090

Note that we use the exact domain name and port as in Caddy file.

There’s one more question - where does the user and key come from?

This is the user that’s going to be used to open ssh connection. Here’s the setup:

  • Create a user on vps, in this example username would be tunnel-user-name but it can be anything
  • Generate SSH key pair
  • Add the public key to ~/.ssh/authorized_keys on VPS
  • Use the private key in the configuration

If you struggle with generating keys, save and run this snippet

#!/bin/bash
echo "SSH keypair generator"
echo "Provide the user you want to generate keys for"
read username

if [[ -z "$username" ]]; then
   echo "Username cannot be empty!"
   exit 1
fi

key_file="./keys/$username"
public_key_file="$key_file.pub"

echo "Key will be created in $key_file"

ssh-keygen -q -t rsa -N '' -f $key_file <<<y >/dev/null 2>&1
chmod 600 $key_file

echo "Files are ready in $key_file and $key_file.pub"

Once you do it, just start the addon and that’s it! If the setup is correct, in the logs you should see an output similar to the one shown below:

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started
[22:30:36] INFO: Reverse tunnel initializing.
[22:30:36] INFO: Variables set, reading configuration.
[22:30:37] INFO: Reverse tunnel configured for tunnel-user-name@homeassistant.mydomain.com
[22:30:37] INFO: Using private key authorization
[22:30:37] INFO: Initializing the ssh tunnel
Warning: Permanently added 'homeassistant.mydomain.com' (ED25519) to the list of known hosts.

The last thing you need is to allow the ingress from reverse proxies. This is done as described in this community discussion. Basically just put this snippet at the end of your config.yaml.

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 172.30.33.0/24
    - 127.0.0.1
    - ::1

Then in Settings > System > Network insert your external hostname (equivalent of homeassistant.mydomain.com form previous snippets) and you’re good to go!

Wrap up

This option is not perfect, but if you can’t get a public IP or prefer not to expose your home network to the internet it sounds like a reasonable thing to try.