Securing Home Assistant Alexa Integration

One of the big missing pieces from my conversion to Home Assistant was Amazon Alexa integration. It wasn’t something we used a lot, but it was a nice to have. Especially for walking out a room and saying “Alexa, turn off the living room lights.”

I had been putting it off a bit because the setup instructions are rather complex. But this weekend I found myself with a couple free hours and decided to work through it. It actually wasn’t as difficult as I expected it to be, but it is definitely not the type of thing a beginner or someone who does not have some programming and sysadmin background could accomplish.

But in working through it, there was one thing that was an immediate red flag for me: the need to expose your Home Assistant installation to the Internet. It makes sense that you would need to do this - the Amazon mothership needs to send data to you to take an action after all. But exposing my entire home automation system to the Internet seems like a really, really bad idea.

So in doing this, rather than expose port 443 on my router to the Internet and open my entire home to a Shodan attack, I decided to try something a bit different.

OpenVPN

So I already have a VM that hosts this site among others. So my first step was to setup an OpenVPN tunnel between my pfSense router and the virtual machine. There are a lot of tutorials out there on how to setup OpenVPN so I won’t duplicate that here.

What you do need to do, though, is add a firewall rule that only allows your tunnel to talk to your Home Assistant installation’s IP address. This way your entire internal network is not potentially exposed should your VM be compromised.

nginx

Next, we’ll configure nginx to act as a proxy between AWS and your internal Home Assistant installation. Here’s the configuration I eventually came up with:

server {
    listen 80;
    server_name home-assistant-proxy.example.com;
    include "/etc/nginx/sites-available/shared/letsencrypt.conf";

    location / {
        return 301 https://home-assistant-proxy.example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name home-assistant-proxy.example.com;

    ssl_certificate     /etc/letsencrypt/live/home-assistant-proxy.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/home-assistant-proxy.example.com/privkey.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location /api/alexa/smart_home {
        limit_except POST {
            deny all;
        }

        if ($http_user_agent !~* ".*Alexa.*us-east") {
            return 403;
        }

        proxy_pass http://home-assistant.example.com;
    }

    location / {
        deny all;
    }
}

So a couple of things about what is going on here.

The port 80 (HTTP) host is basically there to allow LetsEncrypt to create and automatically renew the certificates needed. HTTPS is required to work with Alexa and, while you could get self-signed certificates to work with some additional work, there is really no reason to do that when LetsEncrypt exists and is super easy to use.

Second, we are only proxying one API endpoint: /api/alexa/smart_home, and we are only allowing POST requests to it. When Lambda makes a request to your endpoint, it looks something like this:

54.147.55.16 - - [17/May/2020:16:50:39 +0000] "POST /api/alexa/smart_home HTTP/1.1" 200 1955 "-" "<your Alexa still name> - us-east-1 - python-requests/2.21.0"

We’re also looking at the User Agent string sent with the request and being sure that it looks like it’s coming from AWS Lambda. So only if it passes all of these checks do we proxy it to the home Home Assistant installation. The cool thing is that they send in the User Agent the name of the skill you created, so you could give it a unique ID that only you would know, and check for that in your nginx config for an added level of security.

I would really like to have this locked to specific IP addresses even. But because haaska executes in AWS Lambda it could come from any number of thousands of IP addresses (although in testing so far it seems to originate from about five or ten). The only way to get around this is to do a reverse DNS lookup on the IP and be sure it originated from *.compute-1.amazonaws.com. I intend to do this as well, but it requires building a module, and I haven’t had time to go to that level yet.

Conclusions

Nothing is ever fully secure; it’s all about managing risk. If you can do something that keeps 99% of attacks out, that is a definite win. In this case, my threat model was a Shodan style attack where someone spidering the dynamic IP range assigned to my ISP found port 443 open on my router and found a Home Assistant installation that could be compromised.

This solution massively reduces the chances of something like that happening by:

  • Requiring you to know the hostname that the API will answer on.

  • Only exposing a single API endpoint, not the entire Home Assistant installation.

  • Only allowing one specific type of request with a specific type of user agent.

If you don’t want to go the full on VM and OpenVPN route, you could get at least some protection by following this same workflow of putting an nginx proxy in front of your Home Assistant installation and exposing that to the Internet from your router. While that’s not ideal, it’s still far safer than just exposing Home Assistant to the Internet.

Did something I wrote help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

COVID-19 has taken the world by storm and left a lot of brokenness in its wake. A lot of people are suffering. If you feel so inclined, please make a donation to your local food bank or medical charity. Order take-out from your local Chinese restaurant. Help buy groceries for an unemployed friend. Help people make it through to the other side.

But if you found this article helpful and you really feel like donating to me specifically, you can do so below.

Read More

Migrating from SmartThings to Home Assistant

I have been a SmartThings user for many years. The orginal reason was that, when we bought our current house in 2012, I wanted to turn the eave lights on at sunset and off a few hours later. After a short attempt to use Wifi-based Wemo switches, I settled on SmartThings and GE Z-Wave switches. I was so happy with it that I started putting them in more places. I added Kwikset SmartCode keypad locks and door sensors. I added more switches, like to turn on the garage overhead lights when the doors opened. I added sensors to monitor the temperature in the closet where I keep my server. And for many years this setup worked great. But over the last year, and especially since Samsung acquired SmartThings, I have become increasingly disillusioned with the SmartThings ecosystem. This last week, my disillusionment and frustration finally boiled over, and I migrated to a new platform. So why did I abandon SmartThings?

Internal Auto-Renewing LetsEncrypt Certificates

I have a well-documented obsession with pretty URLs, and this extends even to my internal home network. I have way too much stuff bouncing around in my head to have to remember IP addresses when a domain name is much easier to remember. LetsEncrypt launched to offer free SSL certificates to anyone, but the most crucial feature of their infrastructure, and one someone should have figured out before then, was scriptable automatically renewing certificates. Basically they validate you do in fact own the domain using automated methods, then issue you the new certificate. Thus, your certificates can be renewed on a schedule with no interaction from you. Traditionally, they have done this by placing a file in the webroot and looking for that file before issuing the certificate (see my earlier blog post about Zero Downtime nginx Letsencrypt Certificate Renewals Without the nginx Plugin for more detail about this.) But what happens when you want to issue an internal certificate? One for a service that is not accessible to the outside world, and thus, not visible using the webroot method? Well, it turns out there is a solution for that too!